@promptctl/rich-js 0.18.0 → 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.
- package/README.md +9 -7
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +145 -0
- package/dist/core/box.js +372 -0
- package/dist/core/cells.d.ts +192 -0
- package/dist/core/cells.js +334 -0
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +326 -0
- package/dist/core/color.js +1225 -0
- package/dist/core/console.d.ts +196 -0
- package/dist/core/console.js +596 -0
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +197 -0
- package/dist/core/export-lines.d.ts +130 -0
- package/dist/core/export-lines.js +159 -0
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +33 -0
- package/dist/core/highlighter.js +190 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +140 -0
- package/dist/core/markup.js +654 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +67 -0
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +78 -0
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +49 -0
- package/dist/core/pretty.d.ts +247 -0
- package/dist/core/pretty.js +906 -0
- package/dist/core/protocol.d.ts +305 -0
- package/dist/core/protocol.js +183 -0
- package/dist/core/render.d.ts +87 -0
- package/dist/core/render.js +105 -0
- package/dist/core/segment.d.ts +162 -0
- package/dist/core/segment.js +488 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +152 -0
- package/dist/core/strip.js +326 -0
- package/dist/core/style.d.ts +189 -0
- package/dist/core/style.js +758 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +283 -0
- package/dist/core/text.js +1097 -0
- package/dist/core/wrap.d.ts +46 -0
- package/dist/core/wrap.js +140 -0
- package/dist/host/app.d.ts +133 -0
- package/dist/host/app.js +285 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +180 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +114 -0
- package/dist/node/prompt.d.ts +34 -0
- package/dist/node/prompt.js +51 -0
- package/dist/node/save.d.ts +18 -0
- package/dist/node/save.js +24 -0
- package/dist/node/terminal-host.d.ts +114 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +38 -0
- package/dist/node/traceback.js +98 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +26 -0
- package/dist/renderables/columns.d.ts +77 -0
- package/dist/renderables/columns.js +191 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +31 -0
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +107 -0
- package/dist/renderables/embed.js +160 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +14 -0
- package/dist/renderables/group.js +22 -0
- package/dist/renderables/layout.d.ts +75 -0
- package/dist/renderables/layout.js +223 -0
- package/dist/renderables/live.d.ts +74 -0
- package/dist/renderables/live.js +188 -0
- package/dist/renderables/markdown.d.ts +38 -0
- package/dist/renderables/markdown.js +587 -0
- package/dist/renderables/padding.d.ts +59 -0
- package/dist/renderables/padding.js +147 -0
- package/dist/renderables/panel.d.ts +114 -0
- package/dist/renderables/panel.js +303 -0
- package/dist/renderables/progress.d.ts +178 -0
- package/dist/renderables/progress.js +362 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +64 -0
- package/dist/renderables/prompt.d.ts +71 -0
- package/dist/renderables/prompt.js +165 -0
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +26 -0
- package/dist/renderables/rule.js +86 -0
- package/dist/renderables/spinner.d.ts +57 -0
- package/dist/renderables/spinner.js +103 -0
- package/dist/renderables/status.d.ts +29 -0
- package/dist/renderables/status.js +46 -0
- package/dist/renderables/syntax.d.ts +65 -0
- package/dist/renderables/syntax.js +166 -0
- package/dist/renderables/table.d.ts +232 -0
- package/dist/renderables/table.js +973 -0
- package/dist/renderables/traceback.d.ts +32 -0
- package/dist/renderables/traceback.js +279 -0
- package/dist/renderables/tree.d.ts +76 -0
- package/dist/renderables/tree.js +177 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +196 -0
- package/dist/template-bindings/color-funcs.d.ts +109 -0
- package/dist/template-bindings/color-funcs.js +243 -0
- package/dist/template-bindings/helpers.d.ts +26 -0
- package/dist/template-bindings/helpers.js +33 -0
- package/dist/template-bindings/index.d.ts +119 -0
- package/dist/template-bindings/index.js +162 -0
- package/dist/template-bindings/palette-funcs.d.ts +96 -0
- package/dist/template-bindings/palette-funcs.js +145 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +157 -0
- package/dist/themes/buildPalette.d.ts +57 -0
- package/dist/themes/buildPalette.js +100 -0
- package/dist/themes/colorMath.d.ts +94 -0
- package/dist/themes/colorMath.js +280 -0
- package/dist/themes/colorRef.d.ts +36 -0
- package/dist/themes/colorRef.js +78 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +45 -0
- package/dist/themes/palette.js +71 -0
- package/dist/themes/ramp.d.ts +70 -0
- package/dist/themes/ramp.js +103 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +105 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +45 -0
- package/dist/widgets/button.js +177 -0
- package/dist/widgets/checkbox.d.ts +38 -0
- package/dist/widgets/checkbox.js +135 -0
- package/dist/widgets/dropdown.d.ts +64 -0
- package/dist/widgets/dropdown.js +416 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +54 -0
- package/dist/widgets/slider.js +247 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +283 -0
- package/dist/widgets/text-input.js +1174 -0
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +41 -0
- package/dist/widgets/toggle.js +157 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +65 -0
- package/dist/widgets/widget-app.js +206 -0
- package/dist/widgets/widget-base.d.ts +42 -0
- package/dist/widgets/widget-base.js +172 -0
- package/package.json +16 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OSC 8 hyperlink wire grammar — which bytes may not appear inside a
|
|
3
|
+
* link, how a link becomes bytes, and how bytes are read back as a link.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:one-source-of-truth] The producer (`segmentToString`, the one encoder
|
|
6
|
+
* every link reaches the wire through) opens a link with `osc8Open` and closes
|
|
7
|
+
* it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
|
|
8
|
+
* test extracting URLs) matches them with `OSC8`; the data-model boundary
|
|
9
|
+
* (RichText) cleans URLs with `stripOscTerminators`. All four read the one
|
|
10
|
+
* terminator set below, so the bytes a sanitizer removes and the bytes a
|
|
11
|
+
* reader stops at cannot disagree.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:locality-or-seam] This module depends only on `fnv1a`; every consumer
|
|
14
|
+
* imports downward.
|
|
15
|
+
*
|
|
16
|
+
* The `id=` parameter is what makes one link hover as one link. A terminal
|
|
17
|
+
* treats cells as the same hyperlink when they share BOTH the URI and the id
|
|
18
|
+
* (the OSC 8 spec, and VTE / iTerm2 / kitty / WezTerm alike); without an id,
|
|
19
|
+
* each open sequence is its own link. The encoder writes one OSC 8 pair per
|
|
20
|
+
* segment, as the reference does, so a link whose text spans several segments
|
|
21
|
+
* — a bold glyph beside plain text, a padded cell — is emitted as several
|
|
22
|
+
* pairs, and would highlight piecewise on hover.
|
|
23
|
+
*
|
|
24
|
+
* [LAW:types-are-the-program] The id is a pure function of the URI, so the
|
|
25
|
+
* byte stream stays a pure function of (style, text, destination): no counter,
|
|
26
|
+
* no construction-order dependence. The consequence is deliberate: two spans
|
|
27
|
+
* with the same URI hover as one link wherever they sit on screen, adjacent
|
|
28
|
+
* or not — a click on either does the same thing, so they ARE one link.
|
|
29
|
+
* Because terminals key on the (id, URI) PAIR, a hash collision between two
|
|
30
|
+
* different URIs merges nothing — the URIs still differ — so a 32-bit hash is
|
|
31
|
+
* exact, not approximate.
|
|
32
|
+
*/
|
|
33
|
+
import { fnv1a } from "./fnv1a.js";
|
|
34
|
+
/**
|
|
35
|
+
* The bytes that prematurely terminate an OSC 8 sequence: ESC (`\x1b`, which
|
|
36
|
+
* begins ST `ESC \`), BEL (`\x07`), and the 8-bit ST (`\x9c`).
|
|
37
|
+
*/
|
|
38
|
+
const TERMINATORS = "\\x1b\\x07\\x9c";
|
|
39
|
+
const TERMINATOR_RE = new RegExp(`[${TERMINATORS}]`, "g");
|
|
40
|
+
/** Remove every byte that could break a URL out of its OSC 8 wrap. */
|
|
41
|
+
export function stripOscTerminators(url) {
|
|
42
|
+
return url.replace(TERMINATOR_RE, "");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The bytes that open a hyperlink to `url`.
|
|
46
|
+
*
|
|
47
|
+
* [LAW:single-enforcer] Wire-byte trust boundary — the URL is sanitized at
|
|
48
|
+
* the one place it becomes an OSC 8 sequence, whichever upstream API attached
|
|
49
|
+
* it to the Style (RichText sanitizes at its data-model boundary too; a Style
|
|
50
|
+
* built directly with `new Style({ link })` reaches here unsanitized).
|
|
51
|
+
*/
|
|
52
|
+
export function osc8Open(url) {
|
|
53
|
+
const clean = stripOscTerminators(url);
|
|
54
|
+
// Hex digits are a legal `id=` value: no `:` or `;`.
|
|
55
|
+
return `\x1b]8;id=${fnv1a(clean)};${clean}\x1b\\`;
|
|
56
|
+
}
|
|
57
|
+
/** The bytes that close the current hyperlink. */
|
|
58
|
+
export const OSC8_CLOSE = "\x1b]8;;\x1b\\";
|
|
59
|
+
/**
|
|
60
|
+
* Matches one OSC 8 sequence — an open or a close — terminated by `ESC \`,
|
|
61
|
+
* BEL, or the 8-bit ST. Group 1 is the params, group 2 the URI; both are empty
|
|
62
|
+
* on a close. To read bytes back use `osc8Sequences`; this pattern is exported
|
|
63
|
+
* for composing into a larger one (a width measure's zero-width alternation).
|
|
64
|
+
*/
|
|
65
|
+
export const OSC8 = new RegExp(`\\x1b\\]8;([^;${TERMINATORS}]*);([^${TERMINATORS}]*)(?:\\x1b\\\\|\\x07|\\x9c)`);
|
|
66
|
+
const OSC8_SCAN = new RegExp(OSC8.source, "g");
|
|
67
|
+
/**
|
|
68
|
+
* Every OSC 8 sequence in `text`, in order — the typed way to read rendered
|
|
69
|
+
* bytes back.
|
|
70
|
+
*/
|
|
71
|
+
export function osc8Sequences(text) {
|
|
72
|
+
return [...text.matchAll(OSC8_SCAN)].map((m) => ({
|
|
73
|
+
index: m.index,
|
|
74
|
+
length: m[0].length,
|
|
75
|
+
params: m[1],
|
|
76
|
+
uri: m[2],
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Painting frames over one another on a terminal: the bytes that take it,
|
|
3
|
+
* paint each frame over the last one in place, and hand it back — and, on an
|
|
4
|
+
* output that is not a terminal, the one frame that is left when it is handed
|
|
5
|
+
* back.
|
|
6
|
+
*
|
|
7
|
+
* [LAW:one-source-of-truth] Where the cursor rests between frames, and how
|
|
8
|
+
* many rows the next paint goes back over, is one fact, and a `SurfacePainter`
|
|
9
|
+
* is its only owner: `App` holds one, and so does `Live` on a terminal, so
|
|
10
|
+
* they cannot disagree about it. They once did: `Live` wrote a newline after its last row, and on a
|
|
11
|
+
* frame as tall as the terminal that newline scrolled the screen, so every
|
|
12
|
+
* refresh left the frame's top row behind in scrollback. The cursor rests on
|
|
13
|
+
* the last row, and the newline that ends it is written once, when the frame
|
|
14
|
+
* is handed back.
|
|
15
|
+
*/
|
|
16
|
+
import type { Destination } from "./color.js";
|
|
17
|
+
import type { Height } from "./protocol.js";
|
|
18
|
+
import { Segment } from "./segment.js";
|
|
19
|
+
/**
|
|
20
|
+
* Where a frame is painted. `alternate` is the whole terminal, in the
|
|
21
|
+
* alternate screen buffer, so the rows printed before it are there again when
|
|
22
|
+
* it is handed back. `inline` starts at the cursor's line, as tall as the
|
|
23
|
+
* frame, and stays on the terminal when it is handed back.
|
|
24
|
+
*/
|
|
25
|
+
export type Surface = "alternate" | "inline";
|
|
26
|
+
/** The terminal a frame is painted on, in cells. */
|
|
27
|
+
export interface Screen {
|
|
28
|
+
readonly rows: number;
|
|
29
|
+
readonly cols: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A frame not yet drawn: the painter calls it when it is about to put the
|
|
33
|
+
* frame on the output, so one that never shows a frame never pays to draw it.
|
|
34
|
+
*/
|
|
35
|
+
export type FrameSource = () => Segment[][];
|
|
36
|
+
/**
|
|
37
|
+
* What paints a run of frames on an output, from `take` to `handBack`.
|
|
38
|
+
*/
|
|
39
|
+
export interface Painter {
|
|
40
|
+
/** The budget a frame renders under, on a screen `screenRows` tall. */
|
|
41
|
+
height(screenRows: number): Height;
|
|
42
|
+
/** Take the output. */
|
|
43
|
+
take(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Paint `frame` over the last frame. `frame` is already shaped to its
|
|
46
|
+
* height by whoever set its budget. An empty frame erases the last one.
|
|
47
|
+
*/
|
|
48
|
+
paint(frame: FrameSource, screen: Screen, destination: Destination): void;
|
|
49
|
+
/**
|
|
50
|
+
* The bytes that write `text` while `frame` is the frame standing on the
|
|
51
|
+
* output, for the caller to write in one piece.
|
|
52
|
+
*/
|
|
53
|
+
around(text: string, frame: FrameSource, screen: Screen, destination: Destination): string;
|
|
54
|
+
/** Hand the output back. The next `take` starts from wherever it then is. */
|
|
55
|
+
handBack(): void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Paints frames on one surface of a terminal. It holds the last frame's own
|
|
59
|
+
* rows on the terminal — what the next paint goes back over — from `take` to
|
|
60
|
+
* `handBack`.
|
|
61
|
+
*/
|
|
62
|
+
export declare class SurfacePainter implements Painter {
|
|
63
|
+
private readonly write;
|
|
64
|
+
private readonly geometry;
|
|
65
|
+
private rows;
|
|
66
|
+
constructor(surface: Surface, write: (bytes: string) => void);
|
|
67
|
+
height(screenRows: number): Height;
|
|
68
|
+
/** Take the terminal: the surface entered, the cursor hidden. */
|
|
69
|
+
take(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Paint `frame` over the last frame, leaving the cursor on its last row,
|
|
72
|
+
* and return it as painted. No row is wider than the screen, since one that
|
|
73
|
+
* soft-wrapped would push every row below it down a row the next paint does
|
|
74
|
+
* not go back over.
|
|
75
|
+
*/
|
|
76
|
+
paint(frame: FrameSource, screen: Screen, destination: Destination): Segment[][];
|
|
77
|
+
/**
|
|
78
|
+
* The frame erased, `text` written, and `frame` painted again on the line
|
|
79
|
+
* under it — in one string, so no terminal ever shows the frame gone. The
|
|
80
|
+
* frame fills whole lines, so text that stops mid-line is ended before it.
|
|
81
|
+
*/
|
|
82
|
+
around(text: string, frame: FrameSource, screen: Screen, destination: Destination): string;
|
|
83
|
+
private over;
|
|
84
|
+
/**
|
|
85
|
+
* Hand the terminal back: style reset, then the cursor shown on the
|
|
86
|
+
* program's next line, the surface left.
|
|
87
|
+
*/
|
|
88
|
+
handBack(): void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Paints on an output that is not a terminal — a file, a pipe, a CI log — as
|
|
92
|
+
* Rich's `Live` does: nothing while the frames change, since every one would
|
|
93
|
+
* stay there for good with the escape sequences that move over it as literal
|
|
94
|
+
* bytes, and the last frame once, printed as plain lines through `print`, when
|
|
95
|
+
* the output is handed back. What is written around the frame goes out as it
|
|
96
|
+
* was given. No frame is drawn but the one handed back.
|
|
97
|
+
*/
|
|
98
|
+
export declare class FinalFramePainter implements Painter {
|
|
99
|
+
private readonly print;
|
|
100
|
+
private last;
|
|
101
|
+
constructor(print: (frame: Segment[][]) => void);
|
|
102
|
+
height(screenRows: number): Height;
|
|
103
|
+
take(): void;
|
|
104
|
+
paint(frame: FrameSource): void;
|
|
105
|
+
around(text: string): string;
|
|
106
|
+
handBack(): void;
|
|
107
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Painting frames over one another on a terminal: the bytes that take it,
|
|
3
|
+
* paint each frame over the last one in place, and hand it back — and, on an
|
|
4
|
+
* output that is not a terminal, the one frame that is left when it is handed
|
|
5
|
+
* back.
|
|
6
|
+
*
|
|
7
|
+
* [LAW:one-source-of-truth] Where the cursor rests between frames, and how
|
|
8
|
+
* many rows the next paint goes back over, is one fact, and a `SurfacePainter`
|
|
9
|
+
* is its only owner: `App` holds one, and so does `Live` on a terminal, so
|
|
10
|
+
* they cannot disagree about it. They once did: `Live` wrote a newline after its last row, and on a
|
|
11
|
+
* frame as tall as the terminal that newline scrolled the screen, so every
|
|
12
|
+
* refresh left the frame's top row behind in scrollback. The cursor rests on
|
|
13
|
+
* the last row, and the newline that ends it is written once, when the frame
|
|
14
|
+
* is handed back.
|
|
15
|
+
*/
|
|
16
|
+
import { segmentsToString } from "./render.js";
|
|
17
|
+
import { Segment } from "./segment.js";
|
|
18
|
+
const GEOMETRY = {
|
|
19
|
+
alternate: {
|
|
20
|
+
exact: true,
|
|
21
|
+
enter: "\x1b[?1049h",
|
|
22
|
+
// Leaving the buffer puts back the cursor the program had.
|
|
23
|
+
leave: "\x1b[?1049l",
|
|
24
|
+
home: () => "\x1b[H",
|
|
25
|
+
// The frame is every row of the screen, so there is nothing left under it
|
|
26
|
+
// — and after the terminal shrinks, painting the old count would scroll.
|
|
27
|
+
painted: (frameRows) => frameRows,
|
|
28
|
+
below: () => "",
|
|
29
|
+
},
|
|
30
|
+
inline: {
|
|
31
|
+
exact: false,
|
|
32
|
+
enter: "",
|
|
33
|
+
leave: "",
|
|
34
|
+
// After N rows written with N-1 newlines between them, the cursor is on
|
|
35
|
+
// the last. `ESC[0A` still moves a row on some terminals, so a one-row
|
|
36
|
+
// frame has no move at all.
|
|
37
|
+
home: (rows) => (rows > 1 ? `\x1b[${rows - 1}A` : ""),
|
|
38
|
+
// A shorter frame blanks the rows the last one left below it, as far as
|
|
39
|
+
// the screen still reaches after a shrink.
|
|
40
|
+
painted: (frameRows, lastRows, screenRows) => Math.max(frameRows, Math.min(lastRows, screenRows)),
|
|
41
|
+
// Where the program's next line belongs.
|
|
42
|
+
below: (rows) => (rows > 0 ? "\n" : ""),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
// Each row starts at its line's first cell and is erased as it is reached,
|
|
46
|
+
// rather than the frame cleared first, so no blank screen shows between two
|
|
47
|
+
// frames. The erase leads its row: after a row that fills the width, the
|
|
48
|
+
// cursor sits on its last cell, and an erase there would take it.
|
|
49
|
+
const ROW_START = "\r\x1b[2K";
|
|
50
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
51
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
52
|
+
const RESET_STYLE = "\x1b[0m";
|
|
53
|
+
const fitRows = (frame, screen) => frame.map((line) => Segment.adjustLineLength(line, screen.cols, undefined, false));
|
|
54
|
+
/**
|
|
55
|
+
* Paints frames on one surface of a terminal. It holds the last frame's own
|
|
56
|
+
* rows on the terminal — what the next paint goes back over — from `take` to
|
|
57
|
+
* `handBack`.
|
|
58
|
+
*/
|
|
59
|
+
export class SurfacePainter {
|
|
60
|
+
write;
|
|
61
|
+
geometry;
|
|
62
|
+
rows = 0;
|
|
63
|
+
constructor(surface, write) {
|
|
64
|
+
this.write = write;
|
|
65
|
+
this.geometry = GEOMETRY[surface];
|
|
66
|
+
}
|
|
67
|
+
height(screenRows) {
|
|
68
|
+
return { rows: screenRows, exact: this.geometry.exact };
|
|
69
|
+
}
|
|
70
|
+
/** Take the terminal: the surface entered, the cursor hidden. */
|
|
71
|
+
take() {
|
|
72
|
+
this.write(this.geometry.enter + HIDE_CURSOR);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Paint `frame` over the last frame, leaving the cursor on its last row,
|
|
76
|
+
* and return it as painted. No row is wider than the screen, since one that
|
|
77
|
+
* soft-wrapped would push every row below it down a row the next paint does
|
|
78
|
+
* not go back over.
|
|
79
|
+
*/
|
|
80
|
+
paint(frame, screen, destination) {
|
|
81
|
+
const rows = fitRows(frame(), screen);
|
|
82
|
+
this.write(this.over(rows, screen, destination));
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The frame erased, `text` written, and `frame` painted again on the line
|
|
87
|
+
* under it — in one string, so no terminal ever shows the frame gone. The
|
|
88
|
+
* frame fills whole lines, so text that stops mid-line is ended before it.
|
|
89
|
+
*/
|
|
90
|
+
around(text, frame, screen, destination) {
|
|
91
|
+
// Drawn before `over` moves the row count, so a frame that throws leaves
|
|
92
|
+
// the painter matching the terminal it left untouched.
|
|
93
|
+
const rows = fitRows(frame(), screen);
|
|
94
|
+
const erase = this.over([], screen, destination);
|
|
95
|
+
const ended = text.endsWith("\n") ? text : `${text}\n`;
|
|
96
|
+
return erase + ended + this.over(rows, screen, destination);
|
|
97
|
+
}
|
|
98
|
+
// The bytes that paint `rows` over the last frame. They go out in one
|
|
99
|
+
// string, so whatever takes the last frame away arrives with the new one.
|
|
100
|
+
over(rows, screen, destination) {
|
|
101
|
+
const painted = this.geometry.painted(rows.length, this.rows, screen.rows);
|
|
102
|
+
const body = Array.from({ length: painted }, (_, row) => ROW_START + segmentsToString(rows[row] ?? [], destination)).join("\n");
|
|
103
|
+
// Rows blanked below the frame are not the frame's: the cursor goes back
|
|
104
|
+
// up to its last row, so the next frame and the program's next line start
|
|
105
|
+
// from the frame's own height.
|
|
106
|
+
const blanked = painted - Math.max(rows.length, 1);
|
|
107
|
+
const back = blanked > 0 ? `\x1b[${blanked}A` : "";
|
|
108
|
+
const bytes = this.geometry.home(this.rows) + body + back;
|
|
109
|
+
this.rows = rows.length;
|
|
110
|
+
return bytes;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Hand the terminal back: style reset, then the cursor shown on the
|
|
114
|
+
* program's next line, the surface left.
|
|
115
|
+
*/
|
|
116
|
+
handBack() {
|
|
117
|
+
this.write(RESET_STYLE + SHOW_CURSOR + this.geometry.below(this.rows) + this.geometry.leave);
|
|
118
|
+
this.rows = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Paints on an output that is not a terminal — a file, a pipe, a CI log — as
|
|
123
|
+
* Rich's `Live` does: nothing while the frames change, since every one would
|
|
124
|
+
* stay there for good with the escape sequences that move over it as literal
|
|
125
|
+
* bytes, and the last frame once, printed as plain lines through `print`, when
|
|
126
|
+
* the output is handed back. What is written around the frame goes out as it
|
|
127
|
+
* was given. No frame is drawn but the one handed back.
|
|
128
|
+
*/
|
|
129
|
+
export class FinalFramePainter {
|
|
130
|
+
print;
|
|
131
|
+
last = () => [];
|
|
132
|
+
constructor(print) {
|
|
133
|
+
this.print = print;
|
|
134
|
+
}
|
|
135
|
+
height(screenRows) {
|
|
136
|
+
return { rows: screenRows, exact: false };
|
|
137
|
+
}
|
|
138
|
+
take() { }
|
|
139
|
+
paint(frame) {
|
|
140
|
+
this.last = frame;
|
|
141
|
+
}
|
|
142
|
+
around(text) {
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
handBack() {
|
|
146
|
+
const frame = this.last;
|
|
147
|
+
this.last = () => [];
|
|
148
|
+
this.print(frame());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* place — a renderable's output set down as one block inside a wider width.
|
|
3
|
+
*
|
|
4
|
+
* The block is placed, not its lines. The renderable is measured, drawn at the
|
|
5
|
+
* width it asked for and at its own height — a region's height is dropped, as
|
|
6
|
+
* the reference's `Align` drops it — and every line is padded to the widest, so the block is a
|
|
7
|
+
* rectangle; only then is the rectangle padded to the offered width. A
|
|
8
|
+
* multi-line paragraph keeps its own shape under `"center"` — `hi` over `hello`
|
|
9
|
+
* stays a 5-cell block, centred as one — where padding each line to the full
|
|
10
|
+
* width on its own would centre `hi` against the whole terminal. That is
|
|
11
|
+
* Rich's `Align`, and `test/core/console.test.ts` pins it against the
|
|
12
|
+
* reference's bytes.
|
|
13
|
+
*
|
|
14
|
+
* It lives in `core/` because `Console.print` places with it: Rich's `print`
|
|
15
|
+
* wraps what it prints in `Align` for the three alignments, and `console.ts`
|
|
16
|
+
* reaching for `renderables/align.ts` would be a third upward edge out of this
|
|
17
|
+
* layer. [LAW:one-way-deps] `Align` is this function behind the `Renderable`
|
|
18
|
+
* interface. [LAW:one-source-of-truth]
|
|
19
|
+
*/
|
|
20
|
+
import { Segment } from "./segment.js";
|
|
21
|
+
import type { Renderable, RenderOptions } from "./protocol.js";
|
|
22
|
+
export type Alignment = "left" | "center" | "right";
|
|
23
|
+
/**
|
|
24
|
+
* `renderable`'s lines, the block they form placed by `align` in
|
|
25
|
+
* `options.maxWidth`. `options.maxWidth` must be finite: the spare cells are
|
|
26
|
+
* written out as spaces. A block wider than the offer — a line with nowhere to
|
|
27
|
+
* break, drawn by a renderable that does not cut its own — is left unpadded at
|
|
28
|
+
* its full width, for whatever holds it to crop or keep.
|
|
29
|
+
*/
|
|
30
|
+
export declare function placeBlock(renderable: Renderable, align: Alignment, options: RenderOptions): Segment[][];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* place — a renderable's output set down as one block inside a wider width.
|
|
3
|
+
*
|
|
4
|
+
* The block is placed, not its lines. The renderable is measured, drawn at the
|
|
5
|
+
* width it asked for and at its own height — a region's height is dropped, as
|
|
6
|
+
* the reference's `Align` drops it — and every line is padded to the widest, so the block is a
|
|
7
|
+
* rectangle; only then is the rectangle padded to the offered width. A
|
|
8
|
+
* multi-line paragraph keeps its own shape under `"center"` — `hi` over `hello`
|
|
9
|
+
* stays a 5-cell block, centred as one — where padding each line to the full
|
|
10
|
+
* width on its own would centre `hi` against the whole terminal. That is
|
|
11
|
+
* Rich's `Align`, and `test/core/console.test.ts` pins it against the
|
|
12
|
+
* reference's bytes.
|
|
13
|
+
*
|
|
14
|
+
* It lives in `core/` because `Console.print` places with it: Rich's `print`
|
|
15
|
+
* wraps what it prints in `Align` for the three alignments, and `console.ts`
|
|
16
|
+
* reaching for `renderables/align.ts` would be a third upward edge out of this
|
|
17
|
+
* layer. [LAW:one-way-deps] `Align` is this function behind the `Renderable`
|
|
18
|
+
* interface. [LAW:one-source-of-truth]
|
|
19
|
+
*/
|
|
20
|
+
import { Segment } from "./segment.js";
|
|
21
|
+
import { Measurement } from "./measure.js";
|
|
22
|
+
// The share of the spare cells that goes on the left; the rest go on the right.
|
|
23
|
+
// Right-alignment pads no right side at all, which is the reference's choice
|
|
24
|
+
// and not a zero: a line that ends at the edge carries no trailing blanks.
|
|
25
|
+
const PADS = {
|
|
26
|
+
left: (spare) => [0, spare],
|
|
27
|
+
center: (spare) => [Math.floor(spare / 2), spare - Math.floor(spare / 2)],
|
|
28
|
+
right: (spare) => [spare, 0],
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `renderable`'s lines, the block they form placed by `align` in
|
|
32
|
+
* `options.maxWidth`. `options.maxWidth` must be finite: the spare cells are
|
|
33
|
+
* written out as spaces. A block wider than the offer — a line with nowhere to
|
|
34
|
+
* break, drawn by a renderable that does not cut its own — is left unpadded at
|
|
35
|
+
* its full width, for whatever holds it to crop or keep.
|
|
36
|
+
*/
|
|
37
|
+
export function placeBlock(renderable, align, options) {
|
|
38
|
+
// A renderable that cannot say how wide it wants to be is offered all of it
|
|
39
|
+
// (`Measurement.get` answers for it), and the block is then only as wide as
|
|
40
|
+
// what it actually drew.
|
|
41
|
+
const width = Measurement.get(options, renderable).maximum;
|
|
42
|
+
const drawn = Segment.splitLines(renderable.render({ ...options, maxWidth: width, height: undefined }));
|
|
43
|
+
const [blockWidth, height] = Segment.getShape(drawn);
|
|
44
|
+
const [left, right] = PADS[align](Math.max(0, options.maxWidth - blockWidth));
|
|
45
|
+
return Segment.setShape(drawn, blockWidth, height).map((line) => [...pad(left), ...line, ...pad(right)]);
|
|
46
|
+
}
|
|
47
|
+
// [LAW:dataflow-not-control-flow] no spare room is a pad of no segments, not a
|
|
48
|
+
// step skipped.
|
|
49
|
+
const pad = (cells) => (cells > 0 ? [new Segment(" ".repeat(cells))] : []);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty — formats JavaScript data structures with highlighting.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `core/` beside `markup` and `emoji` because it does their job: turn
|
|
5
|
+
* foreign input into `RichText`. It composes no other renderable — the trait
|
|
6
|
+
* every file in `renderables/` shares and this one does not — and its imports
|
|
7
|
+
* are core primitives only. `Console.print` accepts `unknown` and must turn any
|
|
8
|
+
* of it into something renderable, so the formatter has to sit where `console`
|
|
9
|
+
* can reach it without an upward edge. [LAW:one-way-deps]
|
|
10
|
+
*
|
|
11
|
+
* Two traversals share this file, and the split is the whole design. `_format`
|
|
12
|
+
* lays a value out across lines; `_oneLine` answers only "does this fit on one
|
|
13
|
+
* line, and if so what is it?". They meet at `_shape`, which describes a
|
|
14
|
+
* container without committing to either layout. One traversal answering both
|
|
15
|
+
* questions is what made this formatter exponential: every child was rendered
|
|
16
|
+
* once to probe it and again to place it, so a node at depth d was visited 2^d
|
|
17
|
+
* times.
|
|
18
|
+
*/
|
|
19
|
+
import { Segment } from "./segment.js";
|
|
20
|
+
import { RichText } from "./text.js";
|
|
21
|
+
import type { Highlighter } from "./highlighter.js";
|
|
22
|
+
import type { Renderable, Measurable, RenderOptions } from "./protocol.js";
|
|
23
|
+
export interface PrettyOptions {
|
|
24
|
+
indent?: number;
|
|
25
|
+
expandAll?: boolean;
|
|
26
|
+
maxLength?: number;
|
|
27
|
+
/**
|
|
28
|
+
* How many characters of a string to show before cutting it, counting the
|
|
29
|
+
* rest after the closing quote. A character is a grapheme cluster, not a
|
|
30
|
+
* UTF-16 code unit: `"😀😀"` is two, though its `length` is four.
|
|
31
|
+
*/
|
|
32
|
+
maxString?: number;
|
|
33
|
+
/**
|
|
34
|
+
* How many levels of nesting to descend before eliding. Unbounded by default.
|
|
35
|
+
*
|
|
36
|
+
* `Console.print` supplies a bound because it formats whatever it is handed;
|
|
37
|
+
* a caller constructing a `Pretty` has seen their data and can say what they
|
|
38
|
+
* want. Past the cap a container renders as `[...]`, `{...}`, `Map {...}` or
|
|
39
|
+
* `Set {...}` — visible, so the reader knows the value continues.
|
|
40
|
+
*/
|
|
41
|
+
maxDepth?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Draw a `│` in the first cell of each level of indent. Off by default, as
|
|
44
|
+
* the reference's is, and never drawn on an ASCII-only console, where the
|
|
45
|
+
* reference draws none.
|
|
46
|
+
*/
|
|
47
|
+
indentGuides?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Who colours the formatted text. Defaults to a `ReprHighlighter`.
|
|
50
|
+
*
|
|
51
|
+
* Passed rather than imported so a caller's choice survives. `Console.print`
|
|
52
|
+
* routes data arguments through here, and its `highlight` flag and custom
|
|
53
|
+
* `highlighter` have to reach the output the same way they reach a printed
|
|
54
|
+
* string — a `Pretty` reaching for its own singleton would silently outrank
|
|
55
|
+
* both. `NullHighlighter` is the "none" case.
|
|
56
|
+
*
|
|
57
|
+
* It reads each `Map` entry's `=>` as `= `: the arrow is this formatter's
|
|
58
|
+
* punctuation, not the data's, and its `>` would close a repr tag opened by
|
|
59
|
+
* any `<` before it.
|
|
60
|
+
*/
|
|
61
|
+
highlighter?: Highlighter;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What a value is bounded by when whoever formats it has not seen it:
|
|
65
|
+
* `Console.print`'s data arguments, and a thrown value `Traceback` reports.
|
|
66
|
+
*
|
|
67
|
+
* Such a caller has to assume nothing about the value's size, in any of the
|
|
68
|
+
* three ways a value can be large. Unbounded, a buffer emits a line per byte, a
|
|
69
|
+
* deeply nested object descends until the stack gives out, and a single
|
|
70
|
+
* response body assigned to a field arrives in full — each of them a line that
|
|
71
|
+
* costs megabytes or hangs the terminal. All three bounds announce themselves
|
|
72
|
+
* in the output (`... +N`, `{...}`, `+N` after the closing quote), so this
|
|
73
|
+
* truncates visibly and never silently. [LAW:no-silent-failure]
|
|
74
|
+
*
|
|
75
|
+
* A caller constructing a `Pretty` has seen their data and gets no defaults;
|
|
76
|
+
* these belong to the paths that format what they were handed, not to the
|
|
77
|
+
* formatter.
|
|
78
|
+
*/
|
|
79
|
+
export declare const UNSEEN_DATA_BOUNDS: {
|
|
80
|
+
readonly maxLength: 100;
|
|
81
|
+
readonly maxDepth: 16;
|
|
82
|
+
readonly maxString: 1000;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Whether `Pretty` lays a value out as a container — brackets and positions —
|
|
86
|
+
* rather than spelling it as one piece of text. Emptiness and the depth cap do
|
|
87
|
+
* not enter into it; `[]` is still a container that happens to print on one
|
|
88
|
+
* line. This is Python Rich's `is_expandable`, and `Console.print` reads it to
|
|
89
|
+
* give a container lines of its own.
|
|
90
|
+
*
|
|
91
|
+
* A value whose reflection throws is not one, as in the reference's
|
|
92
|
+
* `_safe_isinstance`: `Pretty` then formats it as text, and the throw reaches
|
|
93
|
+
* the output as `threw`'s marker rather than taking the print down.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isExpandable(value: unknown): boolean;
|
|
96
|
+
export declare class Pretty implements Renderable, Measurable {
|
|
97
|
+
readonly data: unknown;
|
|
98
|
+
readonly indent: number;
|
|
99
|
+
readonly expandAll: boolean;
|
|
100
|
+
readonly maxLength: number | undefined;
|
|
101
|
+
readonly maxString: number | undefined;
|
|
102
|
+
readonly maxDepth: number;
|
|
103
|
+
readonly indentGuides: boolean;
|
|
104
|
+
readonly highlighter: Highlighter;
|
|
105
|
+
private readonly cutStrings;
|
|
106
|
+
constructor(data: unknown, options?: PrettyOptions);
|
|
107
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
108
|
+
/** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
|
|
109
|
+
toText(options: RenderOptions): RichText;
|
|
110
|
+
measure(_options: RenderOptions): {
|
|
111
|
+
minimum: number;
|
|
112
|
+
maximum: number;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The text a value shows without being reflected on, or `null` when it is an
|
|
116
|
+
* object and the shape arms own it.
|
|
117
|
+
*
|
|
118
|
+
* Every kind answered here renders the same wherever it sits — no
|
|
119
|
+
* indentation, no width to fit — which is why one method serves both
|
|
120
|
+
* traversals.
|
|
121
|
+
*/
|
|
122
|
+
private _scalar;
|
|
123
|
+
/**
|
|
124
|
+
* `value` cut to `max` grapheme clusters, with the rest counted after the
|
|
125
|
+
* closing quote.
|
|
126
|
+
*
|
|
127
|
+
* The unit is the cluster of the value, counted before `JSON.stringify`
|
|
128
|
+
* escapes it. A code unit cut can keep half a surrogate pair, which
|
|
129
|
+
* JSON.stringify prints as a `\ud83d` escape; a code point cut can keep half
|
|
130
|
+
* a flag or a ZWJ family. Either way the kept prefix shows something the
|
|
131
|
+
* value never held, and the dropped count is in a unit nobody can see.
|
|
132
|
+
*
|
|
133
|
+
* Counting clusters reads the whole string, and the traversals ask for the
|
|
134
|
+
* same scalar once per probe at every level above it, so the answer is
|
|
135
|
+
* memoised per value.
|
|
136
|
+
*/
|
|
137
|
+
private _cutString;
|
|
138
|
+
/**
|
|
139
|
+
* A container, or the text standing in for one that is empty or past the
|
|
140
|
+
* depth cap.
|
|
141
|
+
*
|
|
142
|
+
* `positions` is a thunk because both of those answers are reachable from
|
|
143
|
+
* `size` alone, and reaching a position is not free: a `Map` or `Set` reaches
|
|
144
|
+
* its own through an iterator, so enumerating one only to elide it drains a
|
|
145
|
+
* container to print `Set {...}`. `size` also says how many positions were
|
|
146
|
+
* dropped, which is where the elision marker comes from — one derivation, so
|
|
147
|
+
* the count and the bound cannot disagree.
|
|
148
|
+
*/
|
|
149
|
+
private _container;
|
|
150
|
+
/**
|
|
151
|
+
* The brackets and positions of an object, layout-free. See `Shape`.
|
|
152
|
+
*
|
|
153
|
+
* `bound` is the most positions the caller could ever use. Laying out passes
|
|
154
|
+
* `Infinity`, because it prints every position it is handed. A probe passes
|
|
155
|
+
* what is left of its line, which is already more positions than a fitting
|
|
156
|
+
* one could hold — `SEPARATOR` charges two cells apiece, so a container with
|
|
157
|
+
* more positions than `budget` overruns however narrow its contents are.
|
|
158
|
+
* Reaching past it only ever feeds a join that returns `null`, and reaching a
|
|
159
|
+
* position is what drains a `Map` or `Set`.
|
|
160
|
+
*/
|
|
161
|
+
private _shape;
|
|
162
|
+
/** The laid-out form of a value, expanded across lines wherever one line will not do. */
|
|
163
|
+
private _format;
|
|
164
|
+
/** The arms for a non-null object, with `value` already on the open path. */
|
|
165
|
+
private _formatObject;
|
|
166
|
+
/**
|
|
167
|
+
* Where a container starts: where it stands, or — when its opener will not
|
|
168
|
+
* fit after what precedes it on its line and will fit on a hanging line —
|
|
169
|
+
* that hanging line, with `lead` the break that reaches it.
|
|
170
|
+
*
|
|
171
|
+
* This is `_place`'s rule for one-piece text, with `open` as the first word:
|
|
172
|
+
* a bracket is a literal no cut can shorten, so moving is the only way it
|
|
173
|
+
* fits. A container moved there is on a line of that indent, its children
|
|
174
|
+
* and closer included, so it reads as its key's value rather than its key's
|
|
175
|
+
* sibling. It moves only to fit: past the edge, the hanging line has no more
|
|
176
|
+
* room than this one.
|
|
177
|
+
*/
|
|
178
|
+
private _opening;
|
|
179
|
+
/**
|
|
180
|
+
* `at` on a line that starts at `inset`. The one place a hang is derived: a
|
|
181
|
+
* line's continuation rows and its children both sit one indent past it.
|
|
182
|
+
*/
|
|
183
|
+
private _onLine;
|
|
184
|
+
/**
|
|
185
|
+
* One position, with every value in it laid out.
|
|
186
|
+
*
|
|
187
|
+
* `at.column` is where `slot.head` begins. Rather than hand-tracking a
|
|
188
|
+
* second, parallel "column so far" alongside `out` — two values a future
|
|
189
|
+
* edit could update out of step, silently reintroducing a stale-budget bug
|
|
190
|
+
* of the same shape this file just fixed — each hole derives its own
|
|
191
|
+
* column fresh from `out` via `lineColumn`, the one place "text just
|
|
192
|
+
* emitted" becomes "column now". [LAW:one-source-of-truth] `out` is
|
|
193
|
+
* already the complete record; `column` is a read of it, not a second copy
|
|
194
|
+
* of the same fact. `_joinOneLine`'s probe sibling tracks the analogous
|
|
195
|
+
* thing via `budget` shrinking instead, because a probe already discards
|
|
196
|
+
* anything that overruns and so never needs to know where a multi-line
|
|
197
|
+
* insert's last line ends. Where that line *starts* is the one fact `out`
|
|
198
|
+
* cannot give back, so it comes from each piece's `Laid.inset` instead.
|
|
199
|
+
*
|
|
200
|
+
* Each hole's `reserve` is its own `tail` plus, only for the slot's last
|
|
201
|
+
* hole, whatever `at.reserve` already asked this whole slot to leave room
|
|
202
|
+
* for (`_formatObject`'s trailing `,`). An earlier hole's tail is never
|
|
203
|
+
* folded into a later hole's reserve — it is spent, not carried, the moment
|
|
204
|
+
* `out` grows past it.
|
|
205
|
+
*/
|
|
206
|
+
private _expandSlot;
|
|
207
|
+
/**
|
|
208
|
+
* Text that is one piece — a scalar, a value that spells itself, a marker —
|
|
209
|
+
* set down at `at.column` on a line this formatter owns.
|
|
210
|
+
*
|
|
211
|
+
* A container that will not fit expands; one piece of text has no structure
|
|
212
|
+
* to expand into, so it wraps, and the wrap is laid out here rather than left
|
|
213
|
+
* to `RichText`. Left there, it broke at the console's edge and resumed at
|
|
214
|
+
* column 0 — under the enclosing key, or under nothing — which is the
|
|
215
|
+
* structure a reader of nested data relies on, broken. So every line after the
|
|
216
|
+
* first hangs at `at.hang`, a line of the value's own (`Error`'s message, a
|
|
217
|
+
* multi-line `toString`) as much as a wrapped one, and each is cut to the
|
|
218
|
+
* width left there. Every row of the last line also leaves `at.reserve`, as a
|
|
219
|
+
* container's compact try does, for the `,` or `" => "` that follows it.
|
|
220
|
+
*
|
|
221
|
+
* The first line is where the text already stands. When not even its first
|
|
222
|
+
* word fits there, it starts on a hanging line instead — the only case that
|
|
223
|
+
* moves it, and one taken only when that line starts further left and so has
|
|
224
|
+
* more room; otherwise the word is folded where it stands. A Map value after
|
|
225
|
+
* a long key is the case: `[1, 2, 3, 10] =>` ends the line and `"v"` hangs
|
|
226
|
+
* beneath it, where it used to wrap to column 0.
|
|
227
|
+
*/
|
|
228
|
+
private _place;
|
|
229
|
+
/** The one-line form of a value, or `null` when it will not fit `at.budget`. */
|
|
230
|
+
private _oneLine;
|
|
231
|
+
/**
|
|
232
|
+
* A container's positions on one line, or `null` as soon as they overrun.
|
|
233
|
+
*
|
|
234
|
+
* The budget is spent as it goes and each position is asked for only what is
|
|
235
|
+
* left, so a subtree that has already outgrown the line is abandoned where it
|
|
236
|
+
* outgrew it rather than formatted in full and then measured.
|
|
237
|
+
*
|
|
238
|
+
* The brackets are charged before anything is read, and that ordering is what
|
|
239
|
+
* bounds the traversal: a budget checked only against the finished text still
|
|
240
|
+
* reads the whole subtree to produce text it then throws away. Charged first,
|
|
241
|
+
* every level costs at least the two cells of its own brackets, so a probe
|
|
242
|
+
* descends at most `budget / 2` levels however deep the data goes.
|
|
243
|
+
*/
|
|
244
|
+
private _joinOneLine;
|
|
245
|
+
/** One position on one line, or `null` when any value in it will not fit. */
|
|
246
|
+
private _slotOneLine;
|
|
247
|
+
}
|