@promptctl/rich-js 0.18.1 → 0.20.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 +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- 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 +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
package/dist/core/pretty.js
CHANGED
|
@@ -16,16 +16,38 @@
|
|
|
16
16
|
* once to probe it and again to place it, so a node at depth d was visited 2^d
|
|
17
17
|
* times.
|
|
18
18
|
*/
|
|
19
|
-
import { cellLen } from "./cells.js";
|
|
20
|
-
import {
|
|
19
|
+
import { asCellCol, cellColToCodeUnitOffset, cellLen, graphemes, splitAtCells } from "./cells.js";
|
|
20
|
+
import { divideLine } from "./wrap.js";
|
|
21
|
+
import { Segment } from "./segment.js";
|
|
22
|
+
import { RichText, stripControlChars } from "./text.js";
|
|
21
23
|
import { ReprHighlighter } from "./highlighter.js";
|
|
24
|
+
import { drawable } from "./protocol.js";
|
|
22
25
|
const reprHighlighter = new ReprHighlighter();
|
|
26
|
+
/**
|
|
27
|
+
* What a value is bounded by when whoever formats it has not seen it:
|
|
28
|
+
* `Console.print`'s data arguments, and a thrown value `Traceback` reports.
|
|
29
|
+
*
|
|
30
|
+
* Such a caller has to assume nothing about the value's size, in any of the
|
|
31
|
+
* three ways a value can be large. Unbounded, a buffer emits a line per byte, a
|
|
32
|
+
* deeply nested object descends until the stack gives out, and a single
|
|
33
|
+
* response body assigned to a field arrives in full — each of them a line that
|
|
34
|
+
* costs megabytes or hangs the terminal. All three bounds announce themselves
|
|
35
|
+
* in the output (`... +N`, `{...}`, `+N` after the closing quote), so this
|
|
36
|
+
* truncates visibly and never silently. [LAW:no-silent-failure]
|
|
37
|
+
*
|
|
38
|
+
* A caller constructing a `Pretty` has seen their data and gets no defaults;
|
|
39
|
+
* these belong to the paths that format what they were handed, not to the
|
|
40
|
+
* formatter.
|
|
41
|
+
*/
|
|
42
|
+
export const UNSEEN_DATA_BOUNDS = { maxLength: 100, maxDepth: 16, maxString: 1000 };
|
|
23
43
|
const rootFrame = (maxWidth) => ({
|
|
24
44
|
inset: 0,
|
|
45
|
+
margin: 0,
|
|
25
46
|
level: 0,
|
|
26
47
|
maxWidth,
|
|
27
48
|
column: 0,
|
|
28
49
|
reserve: 0,
|
|
50
|
+
hang: null,
|
|
29
51
|
open: new WeakSet(),
|
|
30
52
|
});
|
|
31
53
|
/**
|
|
@@ -42,6 +64,148 @@ function lineColumn(column, text) {
|
|
|
42
64
|
const lastNewline = text.lastIndexOf("\n");
|
|
43
65
|
return lastNewline === -1 ? column + cellLen(text) : cellLen(text.slice(lastNewline + 1));
|
|
44
66
|
}
|
|
67
|
+
/** Text with no line break in it. */
|
|
68
|
+
const flat = (text) => ({ text, margins: [], arrows: [] });
|
|
69
|
+
/** A new line whose first `margin` cells are the structure's indent. */
|
|
70
|
+
const newline = (margin) => ({ text: "\n" + " ".repeat(margin), margins: [margin], arrows: [] });
|
|
71
|
+
/** What follows a Map entry's key. */
|
|
72
|
+
const ARROW_TEXT = " => ";
|
|
73
|
+
// [LAW:one-source-of-truth] the arrow's offset is read off its text, never kept beside it.
|
|
74
|
+
const ARROW = { text: ARROW_TEXT, margins: [], arrows: [ARROW_TEXT.indexOf(">")] };
|
|
75
|
+
/** An indent guide: one code unit, as the space it stands over is. */
|
|
76
|
+
const GUIDE = "│";
|
|
77
|
+
const placed = (lines, inset) => ({ ...lines, inset });
|
|
78
|
+
const cat = (...parts) => {
|
|
79
|
+
// `+`, not `join`: a concatenation is left unflattened until it is read, and
|
|
80
|
+
// each level of the data would otherwise copy the whole of its subtree's text.
|
|
81
|
+
let text = "";
|
|
82
|
+
const arrows = [];
|
|
83
|
+
for (const part of parts) {
|
|
84
|
+
for (const arrow of part.arrows)
|
|
85
|
+
arrows.push(text.length + arrow);
|
|
86
|
+
text += part.text;
|
|
87
|
+
}
|
|
88
|
+
return { text, margins: parts.flatMap((part) => part.margins), arrows };
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* A container laid out across lines: `open`, each of `parts` on a line of its
|
|
92
|
+
* own at `inner`, and `close` back at `outer`.
|
|
93
|
+
*
|
|
94
|
+
* Here and `follow` rather than inline because the traversal recurses once per
|
|
95
|
+
* level of the data, and what its frames hold is what bounds the depth it
|
|
96
|
+
* reaches; assembling a piece is done once the recursion under it has returned.
|
|
97
|
+
*/
|
|
98
|
+
function expansion(lead, shape, parts, inner, outer) {
|
|
99
|
+
const lines = parts.map((part, i) => cat(flat(i === 0 ? "" : EXPAND_SEPARATOR), newline(inner), part));
|
|
100
|
+
return placed(cat(lead, flat(shape.open), ...lines, newline(outer), flat(shape.close)), outer);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* `laid` and then `tail` after `out`. A value `_place` moved onto a line of its
|
|
104
|
+
* own leaves the text before it ending the line, and the space that was to
|
|
105
|
+
* separate them goes with it — spaces only, as a line break carries a margin.
|
|
106
|
+
*/
|
|
107
|
+
function follow(out, laid, tail) {
|
|
108
|
+
let end = out.text.length;
|
|
109
|
+
while (laid.text.startsWith("\n") && out.text[end - 1] === " ")
|
|
110
|
+
end--;
|
|
111
|
+
return cat({ ...out, text: out.text.slice(0, end) }, laid, tail);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* `lines` as it is shown, where each run of guides is, and where each of its
|
|
115
|
+
* `arrows` landed. A line with nothing
|
|
116
|
+
* past its margin is blank, and loses the spaces its margin was written as.
|
|
117
|
+
* With a `guide`, each indent of every margin has one in its first cell, as
|
|
118
|
+
* the reference's `with_indent_guides` draws it, blank lines included, so the
|
|
119
|
+
* rule runs unbroken. [LAW:dataflow-not-control-flow] no guide is `null`, and
|
|
120
|
+
* the same pass runs either way.
|
|
121
|
+
*
|
|
122
|
+
* The guide offsets are counted over the text `RichText` will hold, which has
|
|
123
|
+
* its control characters stripped; counted over the raw text, every guide past
|
|
124
|
+
* a `\r` in a key or a `toString` would land one cell right. A margin is only
|
|
125
|
+
* ever spaces, so stripping cannot move one. An arrow is moved by the
|
|
126
|
+
* stripping of what precedes it, and by the blank lines dropped before it;
|
|
127
|
+
* never inside its own line, whose row is the line itself or the same line
|
|
128
|
+
* with guides over its margin. [LAW:types-are-the-program] that holds because
|
|
129
|
+
* the only guide is `GUIDE`, one code unit over one space.
|
|
130
|
+
*/
|
|
131
|
+
function drawMargins(lines, indent, guide) {
|
|
132
|
+
let raw = 0;
|
|
133
|
+
let kept = 0;
|
|
134
|
+
const stripped = lines.arrows.map((arrow) => {
|
|
135
|
+
kept += stripControlChars(lines.text.slice(raw, arrow)).length;
|
|
136
|
+
raw = arrow;
|
|
137
|
+
return kept;
|
|
138
|
+
});
|
|
139
|
+
const arrows = [];
|
|
140
|
+
// Each arrow before `end` in the stripped text is on the line that starts
|
|
141
|
+
// at `from` there and at `to` in what is shown.
|
|
142
|
+
const land = (from, to, end) => {
|
|
143
|
+
while (arrows.length < stripped.length && stripped[arrows.length] < end) {
|
|
144
|
+
arrows.push(to + stripped[arrows.length] - from);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const [first, ...rest] = stripControlChars(lines.text).split("\n");
|
|
148
|
+
const guides = [];
|
|
149
|
+
let offset = first.length;
|
|
150
|
+
let from = first.length;
|
|
151
|
+
land(0, 0, from);
|
|
152
|
+
const rows = rest.map((line, i) => {
|
|
153
|
+
const margin = lines.margins[i];
|
|
154
|
+
const content = line.slice(margin);
|
|
155
|
+
offset += 1;
|
|
156
|
+
from += 1;
|
|
157
|
+
if (guide !== null && margin > 0)
|
|
158
|
+
guides.push([offset, offset + margin]);
|
|
159
|
+
const row = guide === null
|
|
160
|
+
? (content === "" ? "" : line)
|
|
161
|
+
: Array.from({ length: margin }, (_, cell) => (cell % indent === 0 ? guide : " ")).join("") + content;
|
|
162
|
+
land(from, offset, from + line.length);
|
|
163
|
+
offset += row.length;
|
|
164
|
+
from += line.length;
|
|
165
|
+
return row;
|
|
166
|
+
});
|
|
167
|
+
return { plain: [first, ...rows].join("\n"), guides, arrows };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The rows `line` becomes: the first holds what `stay` code units of it leave
|
|
171
|
+
* where the line starts, the rest are cut to `hangRoom` cells each.
|
|
172
|
+
*
|
|
173
|
+
* Every row but the first begins at a break, so whatever whitespace a break
|
|
174
|
+
* leaves on either side of it is the break's and not the text's, and a row of
|
|
175
|
+
* nothing else is no row at all. Each is then set in by the indent the line
|
|
176
|
+
* opened with, so a wrapped line of a multi-line `toString` stays under its own
|
|
177
|
+
* first word.
|
|
178
|
+
*/
|
|
179
|
+
function rowsOf(line, stay, hangRoom) {
|
|
180
|
+
const lead = line.slice(0, line.length - line.trimStart().length);
|
|
181
|
+
const rest = line.slice(stay);
|
|
182
|
+
const hung = splitAtCells(rest, divideLine(rest, asCellCol(hangRoom - cellLen(lead)), { fold: true }))
|
|
183
|
+
.map((row) => row.trim())
|
|
184
|
+
.filter((row) => row !== "")
|
|
185
|
+
.map((row) => lead + row);
|
|
186
|
+
return [line.slice(0, stay).trimEnd(), ...hung];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* How many code units of `line` stay where it starts, with `room` cells left
|
|
190
|
+
* there.
|
|
191
|
+
*
|
|
192
|
+
* What stays is every whole word that fits. When not even the first does,
|
|
193
|
+
* `canHang` says whether a hanging line starts further left than here: if so,
|
|
194
|
+
* nothing stays and all of `line` moves there; if not, moving would gain
|
|
195
|
+
* nothing, and the first word is folded where it stands.
|
|
196
|
+
*/
|
|
197
|
+
function stayingLength(line, room, canHang) {
|
|
198
|
+
if (cellLen(line.trimEnd()) <= room)
|
|
199
|
+
return line.length;
|
|
200
|
+
const cut = (fold) => {
|
|
201
|
+
const first = divideLine(line, asCellCol(room), { fold })[0];
|
|
202
|
+
return first === undefined ? line.length : cellColToCodeUnitOffset(line, first);
|
|
203
|
+
};
|
|
204
|
+
const words = cut(false);
|
|
205
|
+
if (cellLen(line.slice(0, words).trimEnd()) <= room)
|
|
206
|
+
return words;
|
|
207
|
+
return canHang ? 0 : cut(true);
|
|
208
|
+
}
|
|
45
209
|
/**
|
|
46
210
|
* The text, when it is a single line no wider than `budget` — otherwise `null`.
|
|
47
211
|
*
|
|
@@ -51,8 +215,8 @@ function lineColumn(column, text) {
|
|
|
51
215
|
* used to render as `{ m: Map {` — a one-line object with an expansion wedged
|
|
52
216
|
* inside it and the closing brace back at column 0.
|
|
53
217
|
*/
|
|
54
|
-
function fitOneLine(
|
|
55
|
-
return text.includes("\n") || cellLen(text) > budget ? null :
|
|
218
|
+
function fitOneLine(lines, budget) {
|
|
219
|
+
return lines.text.includes("\n") || cellLen(lines.text) > budget ? null : lines;
|
|
56
220
|
}
|
|
57
221
|
/**
|
|
58
222
|
* The elements of an indexed sequence, or `null` for anything that isn't one.
|
|
@@ -72,11 +236,78 @@ function fitOneLine(text, budget) {
|
|
|
72
236
|
function indexedElements(value) {
|
|
73
237
|
if (Array.isArray(value))
|
|
74
238
|
return value;
|
|
75
|
-
if (
|
|
239
|
+
if (typedArrayName(value) !== undefined)
|
|
76
240
|
return value;
|
|
77
|
-
}
|
|
78
241
|
return null;
|
|
79
242
|
}
|
|
243
|
+
/*
|
|
244
|
+
* Brand checks: what a value *is*, asked of the internal slot that makes it
|
|
245
|
+
* one rather than of its prototype chain.
|
|
246
|
+
*
|
|
247
|
+
* `Pretty` reflects on values it did not create, and some were created in
|
|
248
|
+
* another realm — a `vm` context, an iframe — whose `Map`, `DataView` and
|
|
249
|
+
* `Object.prototype` are not this realm's. `instanceof` and prototype identity
|
|
250
|
+
* answer "no" to every one of them, so a `Map` printed `[object Map]` and a
|
|
251
|
+
* `DataView` printed as an empty array. Every built-in method reads its
|
|
252
|
+
* receiver's slot and not its prototype, which is what makes these
|
|
253
|
+
* realm-independent: the platform's own brand check, borrowed from this realm
|
|
254
|
+
* and applied to a value from any.
|
|
255
|
+
*/
|
|
256
|
+
// %TypedArray%.prototype[Symbol.toStringTag]: a typed array's name, and
|
|
257
|
+
// `undefined` for every other receiver, `DataView` included — the spec's brand
|
|
258
|
+
// check for the family, and the one that never throws.
|
|
259
|
+
const typedArrayTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), Symbol.toStringTag)?.get;
|
|
260
|
+
function typedArrayName(value) {
|
|
261
|
+
return typedArrayTag?.call(value);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Is `value` a `Map` (or a `Set`), from this realm or any other?
|
|
265
|
+
*
|
|
266
|
+
* Three kinds of evidence, each the only one that sees some collection.
|
|
267
|
+
* `instanceof` sees this realm's, a `Proxy` around one included. The
|
|
268
|
+
* platform's `Symbol.toStringTag` names the kind in every realm and passes
|
|
269
|
+
* through a `Proxy` — a reactive store made in an iframe — that has neither
|
|
270
|
+
* this realm's prototype nor the slot; if such a proxy will not hand over its
|
|
271
|
+
* entries, reading them throws and prints as `[Threw: …]`, never as an empty
|
|
272
|
+
* collection. The slot sees a subclass from another realm that renamed its
|
|
273
|
+
* tag: `has` throws on any receiver without one. A plain object has no tag at
|
|
274
|
+
* all, so it is never asked — a throw costs a stack trace, too dear to pay for
|
|
275
|
+
* every object printed.
|
|
276
|
+
*/
|
|
277
|
+
function isCollection(value, kind) {
|
|
278
|
+
if (value instanceof kind)
|
|
279
|
+
return true;
|
|
280
|
+
const tag = value[Symbol.toStringTag];
|
|
281
|
+
if (typeof tag !== "string")
|
|
282
|
+
return false;
|
|
283
|
+
if (tag === kind.name)
|
|
284
|
+
return true;
|
|
285
|
+
try {
|
|
286
|
+
kind.prototype.has.call(value, undefined);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function isMap(value) {
|
|
294
|
+
return isCollection(value, Map);
|
|
295
|
+
}
|
|
296
|
+
function isSet(value) {
|
|
297
|
+
return isCollection(value, Set);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Is `fn` the default `toString` of the realm that made it? A function's
|
|
301
|
+
* prototype is its realm's `Function.prototype`, whose own prototype is that
|
|
302
|
+
* realm's `Object.prototype` — two fixed steps from the function to the
|
|
303
|
+
* default it is compared with, so no chain is walked and no identity with this
|
|
304
|
+
* realm's is asked for.
|
|
305
|
+
*/
|
|
306
|
+
function isRealmDefaultToString(fn) {
|
|
307
|
+
const realmFunctionPrototype = Object.getPrototypeOf(fn);
|
|
308
|
+
return (realmFunctionPrototype !== null &&
|
|
309
|
+
Object.getPrototypeOf(realmFunctionPrototype)?.toString === fn);
|
|
310
|
+
}
|
|
80
311
|
/**
|
|
81
312
|
* The first `limit` items of a sequence, pulled and no more.
|
|
82
313
|
*
|
|
@@ -108,7 +339,9 @@ function take(source, limit) {
|
|
|
108
339
|
* lazily-computed field is unusable on the objects it is most wanted for.
|
|
109
340
|
*/
|
|
110
341
|
function threw(error) {
|
|
111
|
-
|
|
342
|
+
// An `Error` from any realm carries its `message` as a string.
|
|
343
|
+
const message = error?.message;
|
|
344
|
+
return `[Threw: ${typeof message === "string" ? message : String(error)}]`;
|
|
112
345
|
}
|
|
113
346
|
/**
|
|
114
347
|
* Does this value carry its own string form, or must we reflect on its keys?
|
|
@@ -122,13 +355,84 @@ function threw(error) {
|
|
|
122
355
|
*
|
|
123
356
|
* Both clauses are load-bearing. The identity check separates the two
|
|
124
357
|
* populations; the `typeof` check is what makes `Object.create(null)` — which
|
|
125
|
-
* has no `toString` at all — reflect rather than throw.
|
|
358
|
+
* has no `toString` at all — reflect rather than throw. The default is the one
|
|
359
|
+
* of the realm that made the `toString`, not this one's: an object from a `vm`
|
|
360
|
+
* context inherits a `toString` that is a different function and the same
|
|
361
|
+
* non-answer.
|
|
126
362
|
*/
|
|
127
363
|
function describesItself(value) {
|
|
128
364
|
const asRecord = value;
|
|
129
365
|
return (typeof asRecord[Symbol.toPrimitive] === "function" ||
|
|
130
366
|
(typeof asRecord.toString === "function" &&
|
|
131
|
-
asRecord.toString
|
|
367
|
+
!isRealmDefaultToString(asRecord.toString)));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The name of an object the platform names but reflection cannot read — a
|
|
371
|
+
* promise, a weak collection, a `WeakRef`, an `ArrayBuffer`, a generator — or
|
|
372
|
+
* `null` when there is no such name or there are keys to show instead. Their
|
|
373
|
+
* state lives in internal slots, so `Object.keys` finds nothing and the keys
|
|
374
|
+
* form would print `{}`, which reads as an empty object. `Symbol.toStringTag` is
|
|
375
|
+
* the kind the platform declares for each, and a subclass and an object from
|
|
376
|
+
* another realm still carry it, so no list of built-ins is kept here. A
|
|
377
|
+
* constructor named otherwise than its tag is named both ways, `Task [Promise]`,
|
|
378
|
+
* in the shape Node's `util.inspect` uses.
|
|
379
|
+
*/
|
|
380
|
+
function opaqueName(value) {
|
|
381
|
+
const tag = value[Symbol.toStringTag];
|
|
382
|
+
if (typeof tag !== "string" || Object.keys(value).length > 0)
|
|
383
|
+
return null;
|
|
384
|
+
const named = Object.getPrototypeOf(value)?.constructor?.name;
|
|
385
|
+
return typeof named === "string" && named !== "" && named !== tag ? `${named} [${tag}]` : tag;
|
|
386
|
+
}
|
|
387
|
+
function formOf(value) {
|
|
388
|
+
const elements = indexedElements(value);
|
|
389
|
+
if (elements !== null)
|
|
390
|
+
return { kind: "indexed", elements };
|
|
391
|
+
if (isMap(value))
|
|
392
|
+
return { kind: "map", map: value };
|
|
393
|
+
if (isSet(value))
|
|
394
|
+
return { kind: "set", set: value };
|
|
395
|
+
// Below the Array/Map/Set arms deliberately: an array also overrides
|
|
396
|
+
// `toString`, but "1,2,3" is a poorer answer than the structural form.
|
|
397
|
+
if (describesItself(value))
|
|
398
|
+
return { kind: "self" };
|
|
399
|
+
// Below `self`, so a promise subclass that spells itself keeps its spelling.
|
|
400
|
+
const opaque = opaqueName(value);
|
|
401
|
+
if (opaque !== null)
|
|
402
|
+
return { kind: "opaque", text: `${opaque} {}` };
|
|
403
|
+
return { kind: "keys", record: value };
|
|
404
|
+
}
|
|
405
|
+
// Which forms have positions to lay out, and which are one piece of text.
|
|
406
|
+
// [LAW:types-are-the-program] Keyed by every kind, so a new form cannot reach
|
|
407
|
+
// `isExpandable` without someone deciding which it is.
|
|
408
|
+
const LAID_OUT = {
|
|
409
|
+
indexed: true,
|
|
410
|
+
map: true,
|
|
411
|
+
set: true,
|
|
412
|
+
keys: true,
|
|
413
|
+
opaque: false,
|
|
414
|
+
self: false,
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* Whether `Pretty` lays a value out as a container — brackets and positions —
|
|
418
|
+
* rather than spelling it as one piece of text. Emptiness and the depth cap do
|
|
419
|
+
* not enter into it; `[]` is still a container that happens to print on one
|
|
420
|
+
* line. This is Python Rich's `is_expandable`, and `Console.print` reads it to
|
|
421
|
+
* give a container lines of its own.
|
|
422
|
+
*
|
|
423
|
+
* A value whose reflection throws is not one, as in the reference's
|
|
424
|
+
* `_safe_isinstance`: `Pretty` then formats it as text, and the throw reaches
|
|
425
|
+
* the output as `threw`'s marker rather than taking the print down.
|
|
426
|
+
*/
|
|
427
|
+
export function isExpandable(value) {
|
|
428
|
+
if (typeof value !== "object" || value === null)
|
|
429
|
+
return false;
|
|
430
|
+
try {
|
|
431
|
+
return LAID_OUT[formOf(value).kind];
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
132
436
|
}
|
|
133
437
|
/** What joins a container's positions on one line. Its width is charged for, so it is named once. */
|
|
134
438
|
const SEPARATOR = ", ";
|
|
@@ -142,7 +446,27 @@ const SEPARATOR = ", ";
|
|
|
142
446
|
*/
|
|
143
447
|
const EXPAND_SEPARATOR = SEPARATOR.trimEnd();
|
|
144
448
|
/** The marker for positions the bound dropped, or nothing when it dropped none. */
|
|
145
|
-
const elided = (dropped) => dropped > 0 ? [{ head: `... +${dropped}`, holes: [] }] : [];
|
|
449
|
+
const elided = (dropped) => dropped > 0 ? [{ head: `... +${dropped}`, join: "", holes: [] }] : [];
|
|
450
|
+
/**
|
|
451
|
+
* A key `Pretty._key` may leave bare. The bare form refuses a default-ignorable code point, which prints as
|
|
452
|
+
* nothing, so `"a\ufe0f"` cannot pass for `a`; `ID_Continue` admits several.
|
|
453
|
+
* An index is at most 15 digits: every such literal is a safe integer and
|
|
454
|
+
* names itself, where `99999999999999999999` would name `1e20`'s key.
|
|
455
|
+
*/
|
|
456
|
+
const BARE_KEY = /^(?![^]*\p{Default_Ignorable_Code_Point})(?:[\p{ID_Start}$_][\p{ID_Continue}$]*|0|[1-9]\d{0,14})$/u;
|
|
457
|
+
/**
|
|
458
|
+
* What Python's `str.isprintable` refuses \u2014 control, format, private-use,
|
|
459
|
+
* unassigned, and every separator but the space \u2014 which Rich's repr escapes
|
|
460
|
+
* and JSON.stringify leaves raw past C0. Raw, DEL vanishes in `RichText`, a C1
|
|
461
|
+
* byte reaches the terminal as a control, and a format character hides or
|
|
462
|
+
* reorders what is around it.
|
|
463
|
+
*/
|
|
464
|
+
const UNPRINTABLE = /(?! )[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/gu;
|
|
465
|
+
/** `value` in double quotes, every character that does not print escaped. */
|
|
466
|
+
const quoted = (value) => JSON.stringify(value).replace(UNPRINTABLE, (c) => {
|
|
467
|
+
const hex = c.codePointAt(0).toString(16).padStart(4, "0");
|
|
468
|
+
return hex.length > 4 ? `\\u{${hex}}` : `\\u${hex}`;
|
|
469
|
+
});
|
|
146
470
|
export class Pretty {
|
|
147
471
|
data;
|
|
148
472
|
indent;
|
|
@@ -152,6 +476,8 @@ export class Pretty {
|
|
|
152
476
|
maxDepth;
|
|
153
477
|
indentGuides;
|
|
154
478
|
highlighter;
|
|
479
|
+
// [LAW:no-shared-mutable-globals] A private memo, written only by `_cutString`.
|
|
480
|
+
cutStrings = new Map();
|
|
155
481
|
constructor(data, options) {
|
|
156
482
|
this.data = data;
|
|
157
483
|
this.indent = options?.indent ?? 4;
|
|
@@ -159,24 +485,40 @@ export class Pretty {
|
|
|
159
485
|
this.maxLength = options?.maxLength;
|
|
160
486
|
this.maxString = options?.maxString;
|
|
161
487
|
this.maxDepth = options?.maxDepth ?? Infinity;
|
|
162
|
-
this.indentGuides = options?.indentGuides
|
|
488
|
+
this.indentGuides = options?.indentGuides ?? false;
|
|
163
489
|
this.highlighter = options?.highlighter ?? reprHighlighter;
|
|
164
490
|
}
|
|
165
491
|
*render(options) {
|
|
492
|
+
// `toText` has no `end`, for callers that set it inside a line; rendered,
|
|
493
|
+
// a `Pretty` is a block of its own, as the reference's is.
|
|
166
494
|
yield* this.toText(options).render(options);
|
|
495
|
+
yield Segment.line();
|
|
167
496
|
}
|
|
168
497
|
/** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
|
|
169
498
|
toText(options) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
499
|
+
const laid = this._format(this.data, rootFrame(options.maxWidth));
|
|
500
|
+
// The reference draws no guide at all on an ASCII-only console, so there is
|
|
501
|
+
// no ASCII glyph to fall back to: `null` is none.
|
|
502
|
+
const guide = this.indentGuides ? drawable(options, GUIDE, null, (g) => g ?? "") : null;
|
|
503
|
+
const { plain, guides, arrows } = drawMargins(laid, this.indent, guide);
|
|
504
|
+
// The highlighter reads the text with each arrow's `>` a space — of the
|
|
505
|
+
// repr patterns only the tag's reads a `>` — and the text then shows it
|
|
506
|
+
// again: one code unit for one, so every span lands where it was found.
|
|
507
|
+
let reading = "";
|
|
508
|
+
let from = 0;
|
|
509
|
+
for (const arrow of arrows) {
|
|
510
|
+
reading += plain.slice(from, arrow) + " ";
|
|
511
|
+
from = arrow + 1;
|
|
175
512
|
}
|
|
513
|
+
const text = new RichText(reading + plain.slice(from), { end: "" });
|
|
514
|
+
this.highlighter.highlight(text);
|
|
515
|
+
text.plain = plain;
|
|
516
|
+
for (const [start, end] of guides)
|
|
517
|
+
text.stylize("repr.indent", start, end);
|
|
176
518
|
return text;
|
|
177
519
|
}
|
|
178
520
|
measure(_options) {
|
|
179
|
-
const formatted = this._format(this.data, rootFrame(_options.maxWidth));
|
|
521
|
+
const formatted = this._format(this.data, rootFrame(_options.maxWidth)).text;
|
|
180
522
|
const lines = formatted.split("\n");
|
|
181
523
|
let max = 0;
|
|
182
524
|
for (const line of lines) {
|
|
@@ -198,17 +540,8 @@ export class Pretty {
|
|
|
198
540
|
if (value === undefined)
|
|
199
541
|
return "undefined";
|
|
200
542
|
switch (typeof value) {
|
|
201
|
-
case "string":
|
|
202
|
-
|
|
203
|
-
// so it goes after the closing quote — the way `maxLength`'s `... +N`
|
|
204
|
-
// sits beside the kept entries rather than inside the last one. Inside
|
|
205
|
-
// the quotes it reads as content, and copying it out yields a string
|
|
206
|
-
// the program never held.
|
|
207
|
-
if (this.maxString === undefined || value.length <= this.maxString) {
|
|
208
|
-
return JSON.stringify(value);
|
|
209
|
-
}
|
|
210
|
-
return JSON.stringify(value.slice(0, this.maxString)) + `+${value.length - this.maxString}`;
|
|
211
|
-
}
|
|
543
|
+
case "string":
|
|
544
|
+
return this._string(value);
|
|
212
545
|
case "number":
|
|
213
546
|
case "bigint":
|
|
214
547
|
case "boolean":
|
|
@@ -220,6 +553,58 @@ export class Pretty {
|
|
|
220
553
|
}
|
|
221
554
|
return null;
|
|
222
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* `value` quoted, and cut to `maxString` when it is longer.
|
|
558
|
+
*
|
|
559
|
+
* The dropped count is an annotation about the value, not part of it, so it
|
|
560
|
+
* goes after the closing quote — the way `maxLength`'s `... +N` sits beside
|
|
561
|
+
* the kept entries rather than inside the last one. Inside the quotes it
|
|
562
|
+
* reads as content, and copying it out yields a string the program never
|
|
563
|
+
* held.
|
|
564
|
+
*
|
|
565
|
+
* A cluster is at least one code unit, so a string no longer than the cap in
|
|
566
|
+
* code units is within it in clusters, and needs no segmenting.
|
|
567
|
+
*/
|
|
568
|
+
_string(value) {
|
|
569
|
+
if (this.maxString === undefined || value.length <= this.maxString)
|
|
570
|
+
return quoted(value);
|
|
571
|
+
return this._cutString(value, this.maxString);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* A key as an object literal would spell it: bare when it is a name or an
|
|
575
|
+
* index, which reads as itself, and quoted as a string otherwise. Rich reprs
|
|
576
|
+
* every key; printed raw, `"a b"` was indistinguishable from two words and a
|
|
577
|
+
* `\r` in one was dropped by `RichText`, so the key shown was not the key held.
|
|
578
|
+
* Quoting only what needs it keeps the JavaScript reading `{ a: 1 }`. A
|
|
579
|
+
* quoted key is a string and `maxString` cuts it; a bare one is a name.
|
|
580
|
+
*/
|
|
581
|
+
_key(key) {
|
|
582
|
+
return BARE_KEY.test(key) ? key : this._string(key);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* `value` cut to `max` grapheme clusters, with the rest counted after the
|
|
586
|
+
* closing quote.
|
|
587
|
+
*
|
|
588
|
+
* The unit is the cluster of the value, counted before `quoted` escapes it. A code unit cut can keep half a surrogate pair, which
|
|
589
|
+
* JSON.stringify prints as a `\ud83d` escape; a code point cut can keep half
|
|
590
|
+
* a flag or a ZWJ family. Either way the kept prefix shows something the
|
|
591
|
+
* value never held, and the dropped count is in a unit nobody can see.
|
|
592
|
+
*
|
|
593
|
+
* Counting clusters reads the whole string, and the traversals ask for the
|
|
594
|
+
* same scalar once per probe at every level above it, so the answer is
|
|
595
|
+
* memoised per value.
|
|
596
|
+
*/
|
|
597
|
+
_cutString(value, max) {
|
|
598
|
+
let cut = this.cutStrings.get(value);
|
|
599
|
+
if (cut === undefined) {
|
|
600
|
+
const clusters = graphemes(value);
|
|
601
|
+
cut = clusters.length <= max
|
|
602
|
+
? quoted(value)
|
|
603
|
+
: quoted(clusters.slice(0, max).join("")) + `+${clusters.length - max}`;
|
|
604
|
+
this.cutStrings.set(value, cut);
|
|
605
|
+
}
|
|
606
|
+
return cut;
|
|
607
|
+
}
|
|
223
608
|
/**
|
|
224
609
|
* A container, or the text standing in for one that is empty or past the
|
|
225
610
|
* depth cap.
|
|
@@ -255,51 +640,57 @@ export class Pretty {
|
|
|
255
640
|
// and `slice` reads a negative end as counting from the far end — it would
|
|
256
641
|
// keep all but the last few keys where the other arms take none.
|
|
257
642
|
const cap = Math.max(0, Math.min(this.maxLength ?? Infinity, bound));
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
643
|
+
const form = formOf(value);
|
|
644
|
+
switch (form.kind) {
|
|
645
|
+
case "indexed": {
|
|
646
|
+
const { elements } = form;
|
|
647
|
+
return this._container("[", "]", "", elements.length, level, () => {
|
|
648
|
+
// Positions rather than values: each index is read in its own slot, so
|
|
649
|
+
// one throwing accessor costs its own slot, not the whole sequence.
|
|
650
|
+
const shown = Math.min(elements.length, cap);
|
|
651
|
+
return Array.from({ length: shown }, (_, i) => ({
|
|
652
|
+
head: "",
|
|
653
|
+
join: "",
|
|
654
|
+
holes: [{ read: () => elements[i], tail: flat("") }],
|
|
655
|
+
}));
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
case "map":
|
|
659
|
+
return this._container("Map {", "}", " ", form.map.size, level, () => take(form.map.entries(), cap).map(([k, v]) => ({
|
|
265
660
|
head: "",
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
661
|
+
join: "",
|
|
662
|
+
holes: [{ read: () => k, tail: ARROW }, { read: () => v, tail: flat("") }],
|
|
663
|
+
})));
|
|
664
|
+
case "set":
|
|
665
|
+
return this._container("Set {", "}", " ", form.set.size, level, () => take(form.set, cap).map((v) => ({
|
|
666
|
+
head: "",
|
|
667
|
+
join: "",
|
|
668
|
+
holes: [{ read: () => v, tail: flat("") }],
|
|
669
|
+
})));
|
|
670
|
+
case "opaque":
|
|
671
|
+
return { kind: "text", text: form.text };
|
|
672
|
+
case "self":
|
|
673
|
+
return { kind: "text", text: String(value) };
|
|
674
|
+
case "keys": {
|
|
675
|
+
// No self-description, so the keys are the whole story.
|
|
676
|
+
const { record } = form;
|
|
677
|
+
const keys = Object.keys(record);
|
|
678
|
+
return this._container("{", "}", " ", keys.length, level, () => keys.slice(0, cap).map((k) => ({
|
|
679
|
+
head: this._key(k),
|
|
680
|
+
join: ": ",
|
|
681
|
+
holes: [{ read: () => record[k], tail: flat("") }],
|
|
682
|
+
})));
|
|
683
|
+
}
|
|
281
684
|
}
|
|
282
|
-
// Objects that answer the display question themselves. Sits below the
|
|
283
|
-
// Array/Map/Set arms deliberately: an array also overrides `toString`, but
|
|
284
|
-
// "1,2,3" is a poorer answer than the structural form above.
|
|
285
|
-
if (describesItself(value))
|
|
286
|
-
return { kind: "text", text: String(value) };
|
|
287
|
-
// Plain objects — no self-description, so the keys are the whole story.
|
|
288
|
-
const obj = value;
|
|
289
|
-
const keys = Object.keys(obj);
|
|
290
|
-
return this._container("{", "}", " ", keys.length, level, () => keys.slice(0, cap).map((k) => ({
|
|
291
|
-
head: `${k}: `,
|
|
292
|
-
holes: [{ read: () => obj[k], tail: "" }],
|
|
293
|
-
})));
|
|
294
685
|
}
|
|
295
686
|
/** The laid-out form of a value, expanded across lines wherever one line will not do. */
|
|
296
687
|
_format(value, at) {
|
|
297
688
|
const scalar = this._scalar(value);
|
|
298
689
|
if (scalar !== null)
|
|
299
|
-
return scalar;
|
|
690
|
+
return this._place(scalar, at);
|
|
300
691
|
const object = value;
|
|
301
692
|
if (at.open.has(object))
|
|
302
|
-
return "[Circular]";
|
|
693
|
+
return this._place("[Circular]", at);
|
|
303
694
|
at.open.add(object);
|
|
304
695
|
try {
|
|
305
696
|
return this._formatObject(object, at);
|
|
@@ -308,17 +699,23 @@ export class Pretty {
|
|
|
308
699
|
// The container's *shape* would not be read — `Object.keys`, an iterator,
|
|
309
700
|
// `toString`. Nothing can be enumerated, so the whole container degrades;
|
|
310
701
|
// a value that merely would not be read degrades alone, in its own slot.
|
|
311
|
-
return threw(error);
|
|
702
|
+
return this._place(threw(error), at);
|
|
312
703
|
}
|
|
313
704
|
finally {
|
|
314
705
|
at.open.delete(object);
|
|
315
706
|
}
|
|
316
707
|
}
|
|
317
708
|
/** The arms for a non-null object, with `value` already on the open path. */
|
|
318
|
-
_formatObject(value,
|
|
319
|
-
const shape = this._shape(value,
|
|
709
|
+
_formatObject(value, from) {
|
|
710
|
+
const shape = this._shape(value, from.level, Infinity);
|
|
320
711
|
if (shape.kind === "text")
|
|
321
|
-
return shape.text;
|
|
712
|
+
return this._place(shape.text, from);
|
|
713
|
+
// Chosen before the compact try, because the choice cannot change its
|
|
714
|
+
// answer where it stands: a line the opener will not fit on cannot hold the
|
|
715
|
+
// one-line form it starts. One pass at the chosen line, and not a second
|
|
716
|
+
// call per move — this method runs once per level of the data, and a
|
|
717
|
+
// thousand levels is within what it is asked to lay out.
|
|
718
|
+
const { lead, at } = this._opening(from, shape.open);
|
|
322
719
|
if (!this.expandAll) {
|
|
323
720
|
// rich-pretty-xms: budget from `at.column` and `at.reserve`, not from
|
|
324
721
|
// `at.inset` alone. `at.column` is where *this* value's text actually
|
|
@@ -336,20 +733,48 @@ export class Pretty {
|
|
|
336
733
|
open: at.open,
|
|
337
734
|
});
|
|
338
735
|
if (compact !== null)
|
|
339
|
-
return compact;
|
|
736
|
+
return placed(cat(lead, compact), at.inset);
|
|
340
737
|
}
|
|
341
|
-
const
|
|
342
|
-
const innerIndent = " ".repeat(this.indent * (at.inset + 1));
|
|
738
|
+
const innerIndent = at.inset + this.indent;
|
|
343
739
|
// Every non-last slot gets `EXPAND_SEPARATOR` appended right after it
|
|
344
740
|
// below, on the same line as whatever its own last character was — the
|
|
345
741
|
// last slot doesn't. Two frames, not one per slot: `reserve` is the only
|
|
346
742
|
// field that varies, and it only ever takes these two values.
|
|
347
|
-
const base = {
|
|
743
|
+
const base = this._onLine({ ...at, level: at.level + 1, column: innerIndent, margin: innerIndent }, innerIndent);
|
|
348
744
|
const midFrame = { ...base, reserve: cellLen(EXPAND_SEPARATOR) };
|
|
349
745
|
const lastFrame = { ...base, reserve: 0 };
|
|
350
746
|
const lastSlot = shape.slots.length - 1;
|
|
351
|
-
const parts = shape.slots.map((slot, i) =>
|
|
352
|
-
return shape
|
|
747
|
+
const parts = shape.slots.map((slot, i) => this._expandSlot(slot, i === lastSlot ? lastFrame : midFrame));
|
|
748
|
+
return expansion(lead, shape, parts, innerIndent, at.inset);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Where a container starts: where it stands, or — when its opener will not
|
|
752
|
+
* fit after what precedes it on its line and will fit on a hanging line —
|
|
753
|
+
* that hanging line, with `lead` the break that reaches it.
|
|
754
|
+
*
|
|
755
|
+
* This is `_place`'s rule for one-piece text, with `open` as the first word:
|
|
756
|
+
* a bracket is a literal no cut can shorten, so moving is the only way it
|
|
757
|
+
* fits. A container moved there is on a line of that indent, its children
|
|
758
|
+
* and closer included, so it reads as its key's value rather than its key's
|
|
759
|
+
* sibling. It moves only to fit: past the edge, the hanging line has no more
|
|
760
|
+
* room than this one.
|
|
761
|
+
*/
|
|
762
|
+
_opening(at, open) {
|
|
763
|
+
const width = cellLen(open);
|
|
764
|
+
if (at.hang === null || at.column + width <= at.maxWidth || at.hang + width > at.maxWidth) {
|
|
765
|
+
return { lead: flat(""), at };
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
lead: newline(at.hang),
|
|
769
|
+
at: { ...this._onLine(at, at.hang), column: at.hang },
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* `at` on a line that starts at `inset`. The one place a hang is derived: a
|
|
774
|
+
* line's continuation rows and its children both sit one indent past it.
|
|
775
|
+
*/
|
|
776
|
+
_onLine(at, inset) {
|
|
777
|
+
return { ...at, inset, hang: inset + this.indent };
|
|
353
778
|
}
|
|
354
779
|
/**
|
|
355
780
|
* One position, with every value in it laid out.
|
|
@@ -364,7 +789,8 @@ export class Pretty {
|
|
|
364
789
|
* of the same fact. `_joinOneLine`'s probe sibling tracks the analogous
|
|
365
790
|
* thing via `budget` shrinking instead, because a probe already discards
|
|
366
791
|
* anything that overruns and so never needs to know where a multi-line
|
|
367
|
-
* insert's last line ends.
|
|
792
|
+
* insert's last line ends. Where that line *starts* is the one fact `out`
|
|
793
|
+
* cannot give back, so it comes from each piece's `Laid.inset` instead.
|
|
368
794
|
*
|
|
369
795
|
* Each hole's `reserve` is its own `tail` plus, only for the slot's last
|
|
370
796
|
* hole, whatever `at.reserve` already asked this whole slot to leave room
|
|
@@ -373,42 +799,92 @@ export class Pretty {
|
|
|
373
799
|
* `out` grows past it.
|
|
374
800
|
*/
|
|
375
801
|
_expandSlot(slot, at) {
|
|
376
|
-
|
|
802
|
+
// A key is one piece of text, placed like any other: one wider than its
|
|
803
|
+
// line wraps under its slot rather than running on to column 0. Only a slot
|
|
804
|
+
// with no values has its `at.reserve` land right after the head; a value
|
|
805
|
+
// may move below it.
|
|
806
|
+
const head = this._place(slot.head, { ...at, reserve: cellLen(slot.join) + (slot.holes.length === 0 ? at.reserve : 0) });
|
|
807
|
+
let out = cat(head, flat(slot.join));
|
|
808
|
+
let inset = head.inset;
|
|
377
809
|
const lastHole = slot.holes.length - 1;
|
|
378
810
|
for (let i = 0; i < slot.holes.length; i++) {
|
|
379
811
|
const hole = slot.holes[i];
|
|
380
812
|
// This is the read that costs the least when it fails: neighbours are
|
|
381
813
|
// unaffected, so `{ a: 1, b: [Threw: …], c: 3 }` still shows everything
|
|
382
814
|
// that could be read.
|
|
383
|
-
|
|
815
|
+
const reserve = cellLen(hole.tail.text) + (i === lastHole ? at.reserve : 0);
|
|
816
|
+
const here = { ...this._onLine(at, inset), column: lineColumn(at.column, out.text), reserve };
|
|
817
|
+
let laid;
|
|
384
818
|
try {
|
|
385
|
-
|
|
386
|
-
text = this._format(hole.read(), { ...at, column: lineColumn(at.column, out), reserve });
|
|
819
|
+
laid = this._format(hole.read(), here);
|
|
387
820
|
}
|
|
388
821
|
catch (error) {
|
|
389
|
-
|
|
822
|
+
laid = this._place(threw(error), here);
|
|
390
823
|
}
|
|
391
|
-
out
|
|
824
|
+
out = follow(out, laid, hole.tail);
|
|
825
|
+
inset = laid.inset;
|
|
392
826
|
}
|
|
393
827
|
return out;
|
|
394
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* Text that is one piece — a scalar, a value that spells itself, a marker —
|
|
831
|
+
* set down at `at.column` on a line this formatter owns.
|
|
832
|
+
*
|
|
833
|
+
* A container that will not fit expands; one piece of text has no structure
|
|
834
|
+
* to expand into, so it wraps, and the wrap is laid out here rather than left
|
|
835
|
+
* to `RichText`. Left there, it broke at the console's edge and resumed at
|
|
836
|
+
* column 0 — under the enclosing key, or under nothing — which is the
|
|
837
|
+
* structure a reader of nested data relies on, broken. So every line after the
|
|
838
|
+
* first hangs at `at.hang`, a line of the value's own (`Error`'s message, a
|
|
839
|
+
* multi-line `toString`) as much as a wrapped one, and each is cut to the
|
|
840
|
+
* width left there. Every row of the last line also leaves `at.reserve`, as a
|
|
841
|
+
* container's compact try does, for the `,` or `" => "` that follows it.
|
|
842
|
+
*
|
|
843
|
+
* The first line is where the text already stands. When not even its first
|
|
844
|
+
* word fits there, it starts on a hanging line instead — the only case that
|
|
845
|
+
* moves it, and one taken only when that line starts further left and so has
|
|
846
|
+
* more room; otherwise the word is folded where it stands. A Map value after
|
|
847
|
+
* a long key is the case: `[1, 2, 3, 10] =>` ends the line and `"v"` hangs
|
|
848
|
+
* beneath it, where it used to wrap to column 0.
|
|
849
|
+
*/
|
|
850
|
+
_place(text, at) {
|
|
851
|
+
// At the root nothing precedes the text that this formatter wrote, so none
|
|
852
|
+
// of its lines has indent that is the structure's.
|
|
853
|
+
if (at.hang === null)
|
|
854
|
+
return placed({ text, margins: text.split("\n").slice(1).map(() => 0), arrows: [] }, at.inset);
|
|
855
|
+
// A hanging row needs a cell to stand in. One indent past the slot has none
|
|
856
|
+
// when the value sits one indent from the edge, and a row put there anyway
|
|
857
|
+
// overruns the width and wraps to column 0 — so rows start where the value
|
|
858
|
+
// began instead, and the first word folds where it stands.
|
|
859
|
+
const hang = at.maxWidth - at.hang - at.reserve > 0 ? at.hang : Math.min(at.hang, at.column);
|
|
860
|
+
const lines = text.split("\n");
|
|
861
|
+
const last = lines.length - 1;
|
|
862
|
+
const room = (i, column) => at.maxWidth - column - (i === last ? at.reserve : 0);
|
|
863
|
+
const [first, ...rows] = lines.flatMap((line, i) => i === 0
|
|
864
|
+
? rowsOf(line, stayingLength(line, room(0, at.column), at.column > hang), room(0, hang))
|
|
865
|
+
: rowsOf(line, stayingLength(line, room(i, hang), false), room(i, hang)));
|
|
866
|
+
// A row hangs past its slot's margin, not at a margin of its own: the
|
|
867
|
+
// cells between are the value's, and a guide there would sit inside it.
|
|
868
|
+
const hung = rows.map((row) => cat(newline(at.margin), flat(row === "" ? "" : " ".repeat(hang - at.margin) + row)));
|
|
869
|
+
return placed(cat(flat(first), ...hung), rows.length === 0 ? at.inset : hang);
|
|
870
|
+
}
|
|
395
871
|
/** The one-line form of a value, or `null` when it will not fit `at.budget`. */
|
|
396
872
|
_oneLine(value, at) {
|
|
397
873
|
const scalar = this._scalar(value);
|
|
398
874
|
if (scalar !== null)
|
|
399
|
-
return fitOneLine(scalar, at.budget);
|
|
875
|
+
return fitOneLine(flat(scalar), at.budget);
|
|
400
876
|
const object = value;
|
|
401
877
|
if (at.open.has(object))
|
|
402
|
-
return fitOneLine("[Circular]", at.budget);
|
|
878
|
+
return fitOneLine(flat("[Circular]"), at.budget);
|
|
403
879
|
at.open.add(object);
|
|
404
880
|
try {
|
|
405
881
|
const shape = this._shape(object, at.level, at.budget);
|
|
406
882
|
if (shape.kind === "text")
|
|
407
|
-
return fitOneLine(shape.text, at.budget);
|
|
883
|
+
return fitOneLine(flat(shape.text), at.budget);
|
|
408
884
|
return this._joinOneLine(shape, { level: at.level + 1, budget: at.budget, open: at.open });
|
|
409
885
|
}
|
|
410
886
|
catch (error) {
|
|
411
|
-
return fitOneLine(threw(error), at.budget);
|
|
887
|
+
return fitOneLine(flat(threw(error)), at.budget);
|
|
412
888
|
}
|
|
413
889
|
finally {
|
|
414
890
|
at.open.delete(object);
|
|
@@ -433,44 +909,34 @@ export class Pretty {
|
|
|
433
909
|
return null;
|
|
434
910
|
const pieces = [];
|
|
435
911
|
for (const slot of shape.slots) {
|
|
436
|
-
|
|
912
|
+
if (pieces.length > 0) {
|
|
913
|
+
used += cellLen(SEPARATOR);
|
|
914
|
+
pieces.push(flat(SEPARATOR));
|
|
915
|
+
}
|
|
437
916
|
const piece = this._slotOneLine(slot, { ...at, budget: at.budget - used });
|
|
438
917
|
if (piece === null)
|
|
439
918
|
return null;
|
|
440
|
-
used += cellLen(piece);
|
|
919
|
+
used += cellLen(piece.text);
|
|
441
920
|
pieces.push(piece);
|
|
442
921
|
}
|
|
443
|
-
return shape.open + shape.pad
|
|
922
|
+
return cat(flat(shape.open + shape.pad), ...pieces, flat(shape.pad + shape.close));
|
|
444
923
|
}
|
|
445
924
|
/** One position on one line, or `null` when any value in it will not fit. */
|
|
446
925
|
_slotOneLine(slot, at) {
|
|
447
|
-
let out = slot.head;
|
|
926
|
+
let out = flat(slot.head + slot.join);
|
|
448
927
|
for (const hole of slot.holes) {
|
|
449
|
-
const left = at.budget - cellLen(out);
|
|
928
|
+
const left = at.budget - cellLen(out.text);
|
|
450
929
|
let text;
|
|
451
930
|
try {
|
|
452
931
|
text = this._oneLine(hole.read(), { ...at, budget: left });
|
|
453
932
|
}
|
|
454
933
|
catch (error) {
|
|
455
|
-
text = fitOneLine(threw(error), left);
|
|
934
|
+
text = fitOneLine(flat(threw(error)), left);
|
|
456
935
|
}
|
|
457
936
|
if (text === null)
|
|
458
937
|
return null;
|
|
459
|
-
out
|
|
938
|
+
out = cat(out, text, hole.tail);
|
|
460
939
|
}
|
|
461
940
|
return fitOneLine(out, at.budget);
|
|
462
941
|
}
|
|
463
|
-
_addIndentGuides(text) {
|
|
464
|
-
const lines = text.plain.split("\n");
|
|
465
|
-
let offset = 0;
|
|
466
|
-
for (const line of lines) {
|
|
467
|
-
const leadingSpaces = line.length - line.trimStart().length;
|
|
468
|
-
for (let i = 0; i < leadingSpaces; i += this.indent) {
|
|
469
|
-
if (i + offset < text.length) {
|
|
470
|
-
text.stylize("repr.indent", offset + i, offset + i + 1);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
offset += line.length + 1; // +1 for newline
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
942
|
}
|