@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.
Files changed (169) hide show
  1. package/README.md +11 -8
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +99 -18
  9. package/dist/core/color.js +334 -146
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/noise.d.ts +31 -0
  33. package/dist/core/noise.js +94 -0
  34. package/dist/core/osc8.d.ts +6 -6
  35. package/dist/core/osc8.js +9 -21
  36. package/dist/core/paint.d.ts +107 -0
  37. package/dist/core/paint.js +161 -0
  38. package/dist/core/place.d.ts +1 -1
  39. package/dist/core/place.js +4 -6
  40. package/dist/core/pretty.d.ts +126 -2
  41. package/dist/core/pretty.js +571 -105
  42. package/dist/core/protocol.d.ts +43 -3
  43. package/dist/core/protocol.js +23 -2
  44. package/dist/core/render.d.ts +33 -30
  45. package/dist/core/render.js +43 -83
  46. package/dist/core/segment.d.ts +6 -5
  47. package/dist/core/segment.js +11 -15
  48. package/dist/core/selection.d.ts +12 -0
  49. package/dist/core/selection.js +14 -0
  50. package/dist/core/strip.d.ts +116 -74
  51. package/dist/core/strip.js +266 -217
  52. package/dist/core/style.d.ts +27 -7
  53. package/dist/core/style.js +106 -41
  54. package/dist/core/text.d.ts +43 -16
  55. package/dist/core/text.js +180 -59
  56. package/dist/core/wrap.d.ts +5 -0
  57. package/dist/core/wrap.js +16 -3
  58. package/dist/host/app.d.ts +95 -17
  59. package/dist/host/app.js +201 -61
  60. package/dist/host/index.d.ts +4 -1
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +84 -0
  63. package/dist/host/input.js +498 -0
  64. package/dist/host/terminal-host.d.ts +15 -2
  65. package/dist/host/terminal-host.js +2 -0
  66. package/dist/index.d.ts +26 -14
  67. package/dist/index.js +17 -7
  68. package/dist/node/prompt.d.ts +13 -0
  69. package/dist/node/prompt.js +30 -9
  70. package/dist/node/save.d.ts +4 -3
  71. package/dist/node/save.js +6 -3
  72. package/dist/node/terminal-host.d.ts +10 -2
  73. package/dist/node/terminal-host.js +7 -0
  74. package/dist/node/traceback.d.ts +2 -1
  75. package/dist/node/traceback.js +3 -39
  76. package/dist/renderables/align.js +2 -9
  77. package/dist/renderables/columns.d.ts +45 -29
  78. package/dist/renderables/columns.js +142 -103
  79. package/dist/renderables/constrain.js +6 -12
  80. package/dist/renderables/effect.d.ts +113 -0
  81. package/dist/renderables/effect.js +182 -0
  82. package/dist/renderables/effects.d.ts +252 -0
  83. package/dist/renderables/effects.js +565 -0
  84. package/dist/renderables/embed.d.ts +94 -31
  85. package/dist/renderables/embed.js +141 -45
  86. package/dist/renderables/group.d.ts +6 -3
  87. package/dist/renderables/group.js +5 -0
  88. package/dist/renderables/layout.d.ts +6 -23
  89. package/dist/renderables/layout.js +24 -113
  90. package/dist/renderables/list-view.d.ts +42 -0
  91. package/dist/renderables/list-view.js +78 -0
  92. package/dist/renderables/live.d.ts +40 -16
  93. package/dist/renderables/live.js +96 -83
  94. package/dist/renderables/markdown.d.ts +16 -2
  95. package/dist/renderables/markdown.js +521 -118
  96. package/dist/renderables/padding.d.ts +22 -1
  97. package/dist/renderables/padding.js +30 -24
  98. package/dist/renderables/panel.d.ts +31 -33
  99. package/dist/renderables/panel.js +137 -141
  100. package/dist/renderables/progress.d.ts +110 -16
  101. package/dist/renderables/progress.js +212 -81
  102. package/dist/renderables/progressBar.d.ts +24 -4
  103. package/dist/renderables/progressBar.js +78 -23
  104. package/dist/renderables/prompt.d.ts +37 -15
  105. package/dist/renderables/prompt.js +125 -85
  106. package/dist/renderables/ratio.d.ts +123 -0
  107. package/dist/renderables/ratio.js +213 -0
  108. package/dist/renderables/rule.d.ts +3 -2
  109. package/dist/renderables/rule.js +50 -52
  110. package/dist/renderables/spinner.d.ts +28 -2
  111. package/dist/renderables/spinner.js +52 -19
  112. package/dist/renderables/status.d.ts +13 -7
  113. package/dist/renderables/status.js +15 -29
  114. package/dist/renderables/syntax.d.ts +37 -7
  115. package/dist/renderables/syntax.js +134 -83
  116. package/dist/renderables/table.d.ts +58 -20
  117. package/dist/renderables/table.js +373 -197
  118. package/dist/renderables/traceback.d.ts +5 -4
  119. package/dist/renderables/traceback.js +239 -68
  120. package/dist/renderables/tree.d.ts +10 -2
  121. package/dist/renderables/tree.js +61 -34
  122. package/dist/renderables/viewport.d.ts +30 -0
  123. package/dist/renderables/viewport.js +61 -7
  124. package/dist/template-bindings/color-funcs.d.ts +31 -9
  125. package/dist/template-bindings/color-funcs.js +40 -14
  126. package/dist/template-bindings/helpers.d.ts +4 -2
  127. package/dist/template-bindings/helpers.js +5 -20
  128. package/dist/template-bindings/index.d.ts +29 -17
  129. package/dist/template-bindings/index.js +36 -20
  130. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  131. package/dist/template-bindings/palette-funcs.js +11 -7
  132. package/dist/template-bindings/style-funcs.js +4 -2
  133. package/dist/themes/buildPalette.d.ts +24 -3
  134. package/dist/themes/buildPalette.js +52 -8
  135. package/dist/themes/colorMath.d.ts +29 -17
  136. package/dist/themes/colorMath.js +74 -48
  137. package/dist/themes/colorRef.d.ts +2 -17
  138. package/dist/themes/colorRef.js +13 -31
  139. package/dist/themes/palette.d.ts +24 -0
  140. package/dist/themes/palette.js +46 -3
  141. package/dist/themes/ramp.d.ts +21 -34
  142. package/dist/themes/ramp.js +23 -42
  143. package/dist/themes/registry.js +39 -28
  144. package/dist/widgets/button.d.ts +2 -6
  145. package/dist/widgets/button.js +9 -40
  146. package/dist/widgets/checkbox.d.ts +2 -5
  147. package/dist/widgets/checkbox.js +9 -31
  148. package/dist/widgets/dropdown.d.ts +2 -5
  149. package/dist/widgets/dropdown.js +17 -51
  150. package/dist/widgets/event-router.d.ts +31 -40
  151. package/dist/widgets/event-router.js +52 -483
  152. package/dist/widgets/index.d.ts +2 -1
  153. package/dist/widgets/ink.d.ts +22 -0
  154. package/dist/widgets/ink.js +31 -0
  155. package/dist/widgets/slider.d.ts +2 -5
  156. package/dist/widgets/slider.js +11 -34
  157. package/dist/widgets/text-input.d.ts +19 -10
  158. package/dist/widgets/text-input.js +156 -147
  159. package/dist/widgets/themed-widget.d.ts +20 -0
  160. package/dist/widgets/themed-widget.js +89 -0
  161. package/dist/widgets/toggle.d.ts +2 -5
  162. package/dist/widgets/toggle.js +8 -31
  163. package/dist/widgets/types.d.ts +8 -25
  164. package/dist/widgets/types.js +8 -0
  165. package/dist/widgets/widget-app.d.ts +24 -7
  166. package/dist/widgets/widget-app.js +35 -18
  167. package/dist/widgets/widget-base.d.ts +2 -0
  168. package/dist/widgets/widget-base.js +9 -2
  169. package/package.json +31 -17
@@ -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 { RichText } from "./text.js";
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(text, budget) {
55
- return text.includes("\n") || cellLen(text) > budget ? null : text;
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 (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
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
- return `[Threw: ${error instanceof Error ? error.message : String(error)}]`;
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 !== Object.prototype.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 !== false;
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 formatted = this._format(this.data, rootFrame(options.maxWidth));
171
- const text = new RichText(formatted, { end: "" });
172
- this.highlighter.highlight(text);
173
- if (this.indentGuides) {
174
- this._addIndentGuides(text);
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
- // The dropped count is an annotation about the value, not part of it,
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 elements = indexedElements(value);
259
- if (elements !== null) {
260
- return this._container("[", "]", "", elements.length, level, () => {
261
- // Positions rather than values: each index is read in its own slot, so
262
- // one throwing accessor costs its own slot, not the whole sequence.
263
- const shown = Math.min(elements.length, cap);
264
- return Array.from({ length: shown }, (_, i) => ({
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
- holes: [{ read: () => elements[i], tail: "" }],
267
- }));
268
- });
269
- }
270
- if (value instanceof Map) {
271
- return this._container("Map {", "}", " ", value.size, level, () => take(value.entries(), cap).map(([k, v]) => ({
272
- head: "",
273
- holes: [{ read: () => k, tail: " => " }, { read: () => v, tail: "" }],
274
- })));
275
- }
276
- if (value instanceof Set) {
277
- return this._container("Set {", "}", " ", value.size, level, () => take(value, cap).map((v) => ({
278
- head: "",
279
- holes: [{ read: () => v, tail: "" }],
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, at) {
319
- const shape = this._shape(value, at.level, Infinity);
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 indentStr = " ".repeat(this.indent * at.inset);
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 = { inset: at.inset + 1, level: at.level + 1, maxWidth: at.maxWidth, column: cellLen(innerIndent), open: at.open };
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) => innerIndent + this._expandSlot(slot, i === lastSlot ? lastFrame : midFrame));
352
- return shape.open + "\n" + parts.join(EXPAND_SEPARATOR + "\n") + "\n" + indentStr + shape.close;
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
- let out = slot.head;
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
- let text;
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
- const reserve = cellLen(hole.tail) + (i === lastHole ? at.reserve : 0);
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
- text = threw(error);
822
+ laid = this._place(threw(error), here);
390
823
  }
391
- out += text + hole.tail;
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
- used += pieces.length > 0 ? cellLen(SEPARATOR) : 0;
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 + pieces.join(SEPARATOR) + shape.pad + shape.close;
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 += text + hole.tail;
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
  }