@promptctl/rich-js 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- 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 +98 -18
- package/dist/core/color.js +331 -144
- 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/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 +150 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +105 -2
- package/dist/core/pretty.js +527 -97
- package/dist/core/protocol.d.ts +41 -3
- package/dist/core/protocol.js +20 -0
- 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/strip.d.ts +13 -2
- package/dist/core/strip.js +44 -23
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +97 -34
- 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 +31 -19
- package/dist/host/app.js +90 -60
- package/dist/host/index.d.ts +2 -1
- package/dist/host/terminal-host.d.ts +3 -2
- package/dist/index.d.ts +17 -10
- package/dist/index.js +11 -4
- 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 +3 -2
- 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 +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +87 -30
- package/dist/renderables/embed.js +134 -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/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 +104 -16
- package/dist/renderables/progress.js +205 -81
- package/dist/renderables/progressBar.d.ts +2 -2
- package/dist/renderables/progressBar.js +30 -19
- 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 +1 -0
- package/dist/renderables/rule.js +44 -48
- 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 +370 -192
- 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.js +2 -4
- 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 +14 -48
- 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/widget-app.d.ts +2 -0
- package/dist/widgets/widget-app.js +13 -6
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +11 -1
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,7 @@ 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: [] }] : [];
|
|
146
450
|
export class Pretty {
|
|
147
451
|
data;
|
|
148
452
|
indent;
|
|
@@ -152,6 +456,8 @@ export class Pretty {
|
|
|
152
456
|
maxDepth;
|
|
153
457
|
indentGuides;
|
|
154
458
|
highlighter;
|
|
459
|
+
// [LAW:no-shared-mutable-globals] A private memo, written only by `_cutString`.
|
|
460
|
+
cutStrings = new Map();
|
|
155
461
|
constructor(data, options) {
|
|
156
462
|
this.data = data;
|
|
157
463
|
this.indent = options?.indent ?? 4;
|
|
@@ -159,24 +465,40 @@ export class Pretty {
|
|
|
159
465
|
this.maxLength = options?.maxLength;
|
|
160
466
|
this.maxString = options?.maxString;
|
|
161
467
|
this.maxDepth = options?.maxDepth ?? Infinity;
|
|
162
|
-
this.indentGuides = options?.indentGuides
|
|
468
|
+
this.indentGuides = options?.indentGuides ?? false;
|
|
163
469
|
this.highlighter = options?.highlighter ?? reprHighlighter;
|
|
164
470
|
}
|
|
165
471
|
*render(options) {
|
|
472
|
+
// `toText` has no `end`, for callers that set it inside a line; rendered,
|
|
473
|
+
// a `Pretty` is a block of its own, as the reference's is.
|
|
166
474
|
yield* this.toText(options).render(options);
|
|
475
|
+
yield Segment.line();
|
|
167
476
|
}
|
|
168
477
|
/** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
|
|
169
478
|
toText(options) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
479
|
+
const laid = this._format(this.data, rootFrame(options.maxWidth));
|
|
480
|
+
// The reference draws no guide at all on an ASCII-only console, so there is
|
|
481
|
+
// no ASCII glyph to fall back to: `null` is none.
|
|
482
|
+
const guide = this.indentGuides ? drawable(options, GUIDE, null, (g) => g ?? "") : null;
|
|
483
|
+
const { plain, guides, arrows } = drawMargins(laid, this.indent, guide);
|
|
484
|
+
// The highlighter reads the text with each arrow's `>` a space — of the
|
|
485
|
+
// repr patterns only the tag's reads a `>` — and the text then shows it
|
|
486
|
+
// again: one code unit for one, so every span lands where it was found.
|
|
487
|
+
let reading = "";
|
|
488
|
+
let from = 0;
|
|
489
|
+
for (const arrow of arrows) {
|
|
490
|
+
reading += plain.slice(from, arrow) + " ";
|
|
491
|
+
from = arrow + 1;
|
|
175
492
|
}
|
|
493
|
+
const text = new RichText(reading + plain.slice(from), { end: "" });
|
|
494
|
+
this.highlighter.highlight(text);
|
|
495
|
+
text.plain = plain;
|
|
496
|
+
for (const [start, end] of guides)
|
|
497
|
+
text.stylize("repr.indent", start, end);
|
|
176
498
|
return text;
|
|
177
499
|
}
|
|
178
500
|
measure(_options) {
|
|
179
|
-
const formatted = this._format(this.data, rootFrame(_options.maxWidth));
|
|
501
|
+
const formatted = this._format(this.data, rootFrame(_options.maxWidth)).text;
|
|
180
502
|
const lines = formatted.split("\n");
|
|
181
503
|
let max = 0;
|
|
182
504
|
for (const line of lines) {
|
|
@@ -204,10 +526,12 @@ export class Pretty {
|
|
|
204
526
|
// sits beside the kept entries rather than inside the last one. Inside
|
|
205
527
|
// the quotes it reads as content, and copying it out yields a string
|
|
206
528
|
// the program never held.
|
|
207
|
-
|
|
529
|
+
//
|
|
530
|
+
// A cluster is at least one code unit, so a string no longer than the
|
|
531
|
+
// cap in code units is within it in clusters, and needs no segmenting.
|
|
532
|
+
if (this.maxString === undefined || value.length <= this.maxString)
|
|
208
533
|
return JSON.stringify(value);
|
|
209
|
-
|
|
210
|
-
return JSON.stringify(value.slice(0, this.maxString)) + `+${value.length - this.maxString}`;
|
|
534
|
+
return this._cutString(value, this.maxString);
|
|
211
535
|
}
|
|
212
536
|
case "number":
|
|
213
537
|
case "bigint":
|
|
@@ -220,6 +544,31 @@ export class Pretty {
|
|
|
220
544
|
}
|
|
221
545
|
return null;
|
|
222
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* `value` cut to `max` grapheme clusters, with the rest counted after the
|
|
549
|
+
* closing quote.
|
|
550
|
+
*
|
|
551
|
+
* The unit is the cluster of the value, counted before `JSON.stringify`
|
|
552
|
+
* escapes it. A code unit cut can keep half a surrogate pair, which
|
|
553
|
+
* JSON.stringify prints as a `\ud83d` escape; a code point cut can keep half
|
|
554
|
+
* a flag or a ZWJ family. Either way the kept prefix shows something the
|
|
555
|
+
* value never held, and the dropped count is in a unit nobody can see.
|
|
556
|
+
*
|
|
557
|
+
* Counting clusters reads the whole string, and the traversals ask for the
|
|
558
|
+
* same scalar once per probe at every level above it, so the answer is
|
|
559
|
+
* memoised per value.
|
|
560
|
+
*/
|
|
561
|
+
_cutString(value, max) {
|
|
562
|
+
let cut = this.cutStrings.get(value);
|
|
563
|
+
if (cut === undefined) {
|
|
564
|
+
const clusters = graphemes(value);
|
|
565
|
+
cut = clusters.length <= max
|
|
566
|
+
? JSON.stringify(value)
|
|
567
|
+
: JSON.stringify(clusters.slice(0, max).join("")) + `+${clusters.length - max}`;
|
|
568
|
+
this.cutStrings.set(value, cut);
|
|
569
|
+
}
|
|
570
|
+
return cut;
|
|
571
|
+
}
|
|
223
572
|
/**
|
|
224
573
|
* A container, or the text standing in for one that is empty or past the
|
|
225
574
|
* depth cap.
|
|
@@ -255,51 +604,57 @@ export class Pretty {
|
|
|
255
604
|
// and `slice` reads a negative end as counting from the far end — it would
|
|
256
605
|
// keep all but the last few keys where the other arms take none.
|
|
257
606
|
const cap = Math.max(0, Math.min(this.maxLength ?? Infinity, bound));
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
607
|
+
const form = formOf(value);
|
|
608
|
+
switch (form.kind) {
|
|
609
|
+
case "indexed": {
|
|
610
|
+
const { elements } = form;
|
|
611
|
+
return this._container("[", "]", "", elements.length, level, () => {
|
|
612
|
+
// Positions rather than values: each index is read in its own slot, so
|
|
613
|
+
// one throwing accessor costs its own slot, not the whole sequence.
|
|
614
|
+
const shown = Math.min(elements.length, cap);
|
|
615
|
+
return Array.from({ length: shown }, (_, i) => ({
|
|
616
|
+
head: "",
|
|
617
|
+
join: "",
|
|
618
|
+
holes: [{ read: () => elements[i], tail: flat("") }],
|
|
619
|
+
}));
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
case "map":
|
|
623
|
+
return this._container("Map {", "}", " ", form.map.size, level, () => take(form.map.entries(), cap).map(([k, v]) => ({
|
|
265
624
|
head: "",
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
625
|
+
join: "",
|
|
626
|
+
holes: [{ read: () => k, tail: ARROW }, { read: () => v, tail: flat("") }],
|
|
627
|
+
})));
|
|
628
|
+
case "set":
|
|
629
|
+
return this._container("Set {", "}", " ", form.set.size, level, () => take(form.set, cap).map((v) => ({
|
|
630
|
+
head: "",
|
|
631
|
+
join: "",
|
|
632
|
+
holes: [{ read: () => v, tail: flat("") }],
|
|
633
|
+
})));
|
|
634
|
+
case "opaque":
|
|
635
|
+
return { kind: "text", text: form.text };
|
|
636
|
+
case "self":
|
|
637
|
+
return { kind: "text", text: String(value) };
|
|
638
|
+
case "keys": {
|
|
639
|
+
// No self-description, so the keys are the whole story.
|
|
640
|
+
const { record } = form;
|
|
641
|
+
const keys = Object.keys(record);
|
|
642
|
+
return this._container("{", "}", " ", keys.length, level, () => keys.slice(0, cap).map((k) => ({
|
|
643
|
+
head: k,
|
|
644
|
+
join: ": ",
|
|
645
|
+
holes: [{ read: () => record[k], tail: flat("") }],
|
|
646
|
+
})));
|
|
647
|
+
}
|
|
281
648
|
}
|
|
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
649
|
}
|
|
295
650
|
/** The laid-out form of a value, expanded across lines wherever one line will not do. */
|
|
296
651
|
_format(value, at) {
|
|
297
652
|
const scalar = this._scalar(value);
|
|
298
653
|
if (scalar !== null)
|
|
299
|
-
return scalar;
|
|
654
|
+
return this._place(scalar, at);
|
|
300
655
|
const object = value;
|
|
301
656
|
if (at.open.has(object))
|
|
302
|
-
return "[Circular]";
|
|
657
|
+
return this._place("[Circular]", at);
|
|
303
658
|
at.open.add(object);
|
|
304
659
|
try {
|
|
305
660
|
return this._formatObject(object, at);
|
|
@@ -308,17 +663,23 @@ export class Pretty {
|
|
|
308
663
|
// The container's *shape* would not be read — `Object.keys`, an iterator,
|
|
309
664
|
// `toString`. Nothing can be enumerated, so the whole container degrades;
|
|
310
665
|
// a value that merely would not be read degrades alone, in its own slot.
|
|
311
|
-
return threw(error);
|
|
666
|
+
return this._place(threw(error), at);
|
|
312
667
|
}
|
|
313
668
|
finally {
|
|
314
669
|
at.open.delete(object);
|
|
315
670
|
}
|
|
316
671
|
}
|
|
317
672
|
/** The arms for a non-null object, with `value` already on the open path. */
|
|
318
|
-
_formatObject(value,
|
|
319
|
-
const shape = this._shape(value,
|
|
673
|
+
_formatObject(value, from) {
|
|
674
|
+
const shape = this._shape(value, from.level, Infinity);
|
|
320
675
|
if (shape.kind === "text")
|
|
321
|
-
return shape.text;
|
|
676
|
+
return this._place(shape.text, from);
|
|
677
|
+
// Chosen before the compact try, because the choice cannot change its
|
|
678
|
+
// answer where it stands: a line the opener will not fit on cannot hold the
|
|
679
|
+
// one-line form it starts. One pass at the chosen line, and not a second
|
|
680
|
+
// call per move — this method runs once per level of the data, and a
|
|
681
|
+
// thousand levels is within what it is asked to lay out.
|
|
682
|
+
const { lead, at } = this._opening(from, shape.open);
|
|
322
683
|
if (!this.expandAll) {
|
|
323
684
|
// rich-pretty-xms: budget from `at.column` and `at.reserve`, not from
|
|
324
685
|
// `at.inset` alone. `at.column` is where *this* value's text actually
|
|
@@ -336,20 +697,48 @@ export class Pretty {
|
|
|
336
697
|
open: at.open,
|
|
337
698
|
});
|
|
338
699
|
if (compact !== null)
|
|
339
|
-
return compact;
|
|
700
|
+
return placed(cat(lead, compact), at.inset);
|
|
340
701
|
}
|
|
341
|
-
const
|
|
342
|
-
const innerIndent = " ".repeat(this.indent * (at.inset + 1));
|
|
702
|
+
const innerIndent = at.inset + this.indent;
|
|
343
703
|
// Every non-last slot gets `EXPAND_SEPARATOR` appended right after it
|
|
344
704
|
// below, on the same line as whatever its own last character was — the
|
|
345
705
|
// last slot doesn't. Two frames, not one per slot: `reserve` is the only
|
|
346
706
|
// field that varies, and it only ever takes these two values.
|
|
347
|
-
const base = {
|
|
707
|
+
const base = this._onLine({ ...at, level: at.level + 1, column: innerIndent, margin: innerIndent }, innerIndent);
|
|
348
708
|
const midFrame = { ...base, reserve: cellLen(EXPAND_SEPARATOR) };
|
|
349
709
|
const lastFrame = { ...base, reserve: 0 };
|
|
350
710
|
const lastSlot = shape.slots.length - 1;
|
|
351
|
-
const parts = shape.slots.map((slot, i) =>
|
|
352
|
-
return shape
|
|
711
|
+
const parts = shape.slots.map((slot, i) => this._expandSlot(slot, i === lastSlot ? lastFrame : midFrame));
|
|
712
|
+
return expansion(lead, shape, parts, innerIndent, at.inset);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Where a container starts: where it stands, or — when its opener will not
|
|
716
|
+
* fit after what precedes it on its line and will fit on a hanging line —
|
|
717
|
+
* that hanging line, with `lead` the break that reaches it.
|
|
718
|
+
*
|
|
719
|
+
* This is `_place`'s rule for one-piece text, with `open` as the first word:
|
|
720
|
+
* a bracket is a literal no cut can shorten, so moving is the only way it
|
|
721
|
+
* fits. A container moved there is on a line of that indent, its children
|
|
722
|
+
* and closer included, so it reads as its key's value rather than its key's
|
|
723
|
+
* sibling. It moves only to fit: past the edge, the hanging line has no more
|
|
724
|
+
* room than this one.
|
|
725
|
+
*/
|
|
726
|
+
_opening(at, open) {
|
|
727
|
+
const width = cellLen(open);
|
|
728
|
+
if (at.hang === null || at.column + width <= at.maxWidth || at.hang + width > at.maxWidth) {
|
|
729
|
+
return { lead: flat(""), at };
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
lead: newline(at.hang),
|
|
733
|
+
at: { ...this._onLine(at, at.hang), column: at.hang },
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* `at` on a line that starts at `inset`. The one place a hang is derived: a
|
|
738
|
+
* line's continuation rows and its children both sit one indent past it.
|
|
739
|
+
*/
|
|
740
|
+
_onLine(at, inset) {
|
|
741
|
+
return { ...at, inset, hang: inset + this.indent };
|
|
353
742
|
}
|
|
354
743
|
/**
|
|
355
744
|
* One position, with every value in it laid out.
|
|
@@ -364,7 +753,8 @@ export class Pretty {
|
|
|
364
753
|
* of the same fact. `_joinOneLine`'s probe sibling tracks the analogous
|
|
365
754
|
* thing via `budget` shrinking instead, because a probe already discards
|
|
366
755
|
* anything that overruns and so never needs to know where a multi-line
|
|
367
|
-
* insert's last line ends.
|
|
756
|
+
* insert's last line ends. Where that line *starts* is the one fact `out`
|
|
757
|
+
* cannot give back, so it comes from each piece's `Laid.inset` instead.
|
|
368
758
|
*
|
|
369
759
|
* Each hole's `reserve` is its own `tail` plus, only for the slot's last
|
|
370
760
|
* hole, whatever `at.reserve` already asked this whole slot to leave room
|
|
@@ -373,42 +763,92 @@ export class Pretty {
|
|
|
373
763
|
* `out` grows past it.
|
|
374
764
|
*/
|
|
375
765
|
_expandSlot(slot, at) {
|
|
376
|
-
|
|
766
|
+
// A key is one piece of text, placed like any other: one wider than its
|
|
767
|
+
// line wraps under its slot rather than running on to column 0. Only a slot
|
|
768
|
+
// with no values has its `at.reserve` land right after the head; a value
|
|
769
|
+
// may move below it.
|
|
770
|
+
const head = this._place(slot.head, { ...at, reserve: cellLen(slot.join) + (slot.holes.length === 0 ? at.reserve : 0) });
|
|
771
|
+
let out = cat(head, flat(slot.join));
|
|
772
|
+
let inset = head.inset;
|
|
377
773
|
const lastHole = slot.holes.length - 1;
|
|
378
774
|
for (let i = 0; i < slot.holes.length; i++) {
|
|
379
775
|
const hole = slot.holes[i];
|
|
380
776
|
// This is the read that costs the least when it fails: neighbours are
|
|
381
777
|
// unaffected, so `{ a: 1, b: [Threw: …], c: 3 }` still shows everything
|
|
382
778
|
// that could be read.
|
|
383
|
-
|
|
779
|
+
const reserve = cellLen(hole.tail.text) + (i === lastHole ? at.reserve : 0);
|
|
780
|
+
const here = { ...this._onLine(at, inset), column: lineColumn(at.column, out.text), reserve };
|
|
781
|
+
let laid;
|
|
384
782
|
try {
|
|
385
|
-
|
|
386
|
-
text = this._format(hole.read(), { ...at, column: lineColumn(at.column, out), reserve });
|
|
783
|
+
laid = this._format(hole.read(), here);
|
|
387
784
|
}
|
|
388
785
|
catch (error) {
|
|
389
|
-
|
|
786
|
+
laid = this._place(threw(error), here);
|
|
390
787
|
}
|
|
391
|
-
out
|
|
788
|
+
out = follow(out, laid, hole.tail);
|
|
789
|
+
inset = laid.inset;
|
|
392
790
|
}
|
|
393
791
|
return out;
|
|
394
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Text that is one piece — a scalar, a value that spells itself, a marker —
|
|
795
|
+
* set down at `at.column` on a line this formatter owns.
|
|
796
|
+
*
|
|
797
|
+
* A container that will not fit expands; one piece of text has no structure
|
|
798
|
+
* to expand into, so it wraps, and the wrap is laid out here rather than left
|
|
799
|
+
* to `RichText`. Left there, it broke at the console's edge and resumed at
|
|
800
|
+
* column 0 — under the enclosing key, or under nothing — which is the
|
|
801
|
+
* structure a reader of nested data relies on, broken. So every line after the
|
|
802
|
+
* first hangs at `at.hang`, a line of the value's own (`Error`'s message, a
|
|
803
|
+
* multi-line `toString`) as much as a wrapped one, and each is cut to the
|
|
804
|
+
* width left there. Every row of the last line also leaves `at.reserve`, as a
|
|
805
|
+
* container's compact try does, for the `,` or `" => "` that follows it.
|
|
806
|
+
*
|
|
807
|
+
* The first line is where the text already stands. When not even its first
|
|
808
|
+
* word fits there, it starts on a hanging line instead — the only case that
|
|
809
|
+
* moves it, and one taken only when that line starts further left and so has
|
|
810
|
+
* more room; otherwise the word is folded where it stands. A Map value after
|
|
811
|
+
* a long key is the case: `[1, 2, 3, 10] =>` ends the line and `"v"` hangs
|
|
812
|
+
* beneath it, where it used to wrap to column 0.
|
|
813
|
+
*/
|
|
814
|
+
_place(text, at) {
|
|
815
|
+
// At the root nothing precedes the text that this formatter wrote, so none
|
|
816
|
+
// of its lines has indent that is the structure's.
|
|
817
|
+
if (at.hang === null)
|
|
818
|
+
return placed({ text, margins: text.split("\n").slice(1).map(() => 0), arrows: [] }, at.inset);
|
|
819
|
+
// A hanging row needs a cell to stand in. One indent past the slot has none
|
|
820
|
+
// when the value sits one indent from the edge, and a row put there anyway
|
|
821
|
+
// overruns the width and wraps to column 0 — so rows start where the value
|
|
822
|
+
// began instead, and the first word folds where it stands.
|
|
823
|
+
const hang = at.maxWidth - at.hang - at.reserve > 0 ? at.hang : Math.min(at.hang, at.column);
|
|
824
|
+
const lines = text.split("\n");
|
|
825
|
+
const last = lines.length - 1;
|
|
826
|
+
const room = (i, column) => at.maxWidth - column - (i === last ? at.reserve : 0);
|
|
827
|
+
const [first, ...rows] = lines.flatMap((line, i) => i === 0
|
|
828
|
+
? rowsOf(line, stayingLength(line, room(0, at.column), at.column > hang), room(0, hang))
|
|
829
|
+
: rowsOf(line, stayingLength(line, room(i, hang), false), room(i, hang)));
|
|
830
|
+
// A row hangs past its slot's margin, not at a margin of its own: the
|
|
831
|
+
// cells between are the value's, and a guide there would sit inside it.
|
|
832
|
+
const hung = rows.map((row) => cat(newline(at.margin), flat(row === "" ? "" : " ".repeat(hang - at.margin) + row)));
|
|
833
|
+
return placed(cat(flat(first), ...hung), rows.length === 0 ? at.inset : hang);
|
|
834
|
+
}
|
|
395
835
|
/** The one-line form of a value, or `null` when it will not fit `at.budget`. */
|
|
396
836
|
_oneLine(value, at) {
|
|
397
837
|
const scalar = this._scalar(value);
|
|
398
838
|
if (scalar !== null)
|
|
399
|
-
return fitOneLine(scalar, at.budget);
|
|
839
|
+
return fitOneLine(flat(scalar), at.budget);
|
|
400
840
|
const object = value;
|
|
401
841
|
if (at.open.has(object))
|
|
402
|
-
return fitOneLine("[Circular]", at.budget);
|
|
842
|
+
return fitOneLine(flat("[Circular]"), at.budget);
|
|
403
843
|
at.open.add(object);
|
|
404
844
|
try {
|
|
405
845
|
const shape = this._shape(object, at.level, at.budget);
|
|
406
846
|
if (shape.kind === "text")
|
|
407
|
-
return fitOneLine(shape.text, at.budget);
|
|
847
|
+
return fitOneLine(flat(shape.text), at.budget);
|
|
408
848
|
return this._joinOneLine(shape, { level: at.level + 1, budget: at.budget, open: at.open });
|
|
409
849
|
}
|
|
410
850
|
catch (error) {
|
|
411
|
-
return fitOneLine(threw(error), at.budget);
|
|
851
|
+
return fitOneLine(flat(threw(error)), at.budget);
|
|
412
852
|
}
|
|
413
853
|
finally {
|
|
414
854
|
at.open.delete(object);
|
|
@@ -433,44 +873,34 @@ export class Pretty {
|
|
|
433
873
|
return null;
|
|
434
874
|
const pieces = [];
|
|
435
875
|
for (const slot of shape.slots) {
|
|
436
|
-
|
|
876
|
+
if (pieces.length > 0) {
|
|
877
|
+
used += cellLen(SEPARATOR);
|
|
878
|
+
pieces.push(flat(SEPARATOR));
|
|
879
|
+
}
|
|
437
880
|
const piece = this._slotOneLine(slot, { ...at, budget: at.budget - used });
|
|
438
881
|
if (piece === null)
|
|
439
882
|
return null;
|
|
440
|
-
used += cellLen(piece);
|
|
883
|
+
used += cellLen(piece.text);
|
|
441
884
|
pieces.push(piece);
|
|
442
885
|
}
|
|
443
|
-
return shape.open + shape.pad
|
|
886
|
+
return cat(flat(shape.open + shape.pad), ...pieces, flat(shape.pad + shape.close));
|
|
444
887
|
}
|
|
445
888
|
/** One position on one line, or `null` when any value in it will not fit. */
|
|
446
889
|
_slotOneLine(slot, at) {
|
|
447
|
-
let out = slot.head;
|
|
890
|
+
let out = flat(slot.head + slot.join);
|
|
448
891
|
for (const hole of slot.holes) {
|
|
449
|
-
const left = at.budget - cellLen(out);
|
|
892
|
+
const left = at.budget - cellLen(out.text);
|
|
450
893
|
let text;
|
|
451
894
|
try {
|
|
452
895
|
text = this._oneLine(hole.read(), { ...at, budget: left });
|
|
453
896
|
}
|
|
454
897
|
catch (error) {
|
|
455
|
-
text = fitOneLine(threw(error), left);
|
|
898
|
+
text = fitOneLine(flat(threw(error)), left);
|
|
456
899
|
}
|
|
457
900
|
if (text === null)
|
|
458
901
|
return null;
|
|
459
|
-
out
|
|
902
|
+
out = cat(out, text, hole.tail);
|
|
460
903
|
}
|
|
461
904
|
return fitOneLine(out, at.budget);
|
|
462
905
|
}
|
|
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
906
|
}
|