@promptctl/rich-js 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +9 -7
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +98 -18
  9. package/dist/core/color.js +331 -144
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/osc8.d.ts +6 -6
  33. package/dist/core/osc8.js +9 -21
  34. package/dist/core/paint.d.ts +107 -0
  35. package/dist/core/paint.js +150 -0
  36. package/dist/core/place.d.ts +1 -1
  37. package/dist/core/place.js +4 -6
  38. package/dist/core/pretty.d.ts +105 -2
  39. package/dist/core/pretty.js +527 -97
  40. package/dist/core/protocol.d.ts +41 -3
  41. package/dist/core/protocol.js +20 -0
  42. package/dist/core/render.d.ts +33 -30
  43. package/dist/core/render.js +43 -83
  44. package/dist/core/segment.d.ts +6 -5
  45. package/dist/core/segment.js +11 -15
  46. package/dist/core/strip.d.ts +13 -2
  47. package/dist/core/strip.js +44 -23
  48. package/dist/core/style.d.ts +27 -7
  49. package/dist/core/style.js +97 -34
  50. package/dist/core/text.d.ts +43 -16
  51. package/dist/core/text.js +180 -59
  52. package/dist/core/wrap.d.ts +5 -0
  53. package/dist/core/wrap.js +16 -3
  54. package/dist/host/app.d.ts +31 -19
  55. package/dist/host/app.js +90 -60
  56. package/dist/host/index.d.ts +2 -1
  57. package/dist/host/terminal-host.d.ts +3 -2
  58. package/dist/index.d.ts +17 -10
  59. package/dist/index.js +11 -4
  60. package/dist/node/prompt.d.ts +13 -0
  61. package/dist/node/prompt.js +30 -9
  62. package/dist/node/save.d.ts +4 -3
  63. package/dist/node/save.js +6 -3
  64. package/dist/node/terminal-host.d.ts +3 -2
  65. package/dist/node/traceback.d.ts +2 -1
  66. package/dist/node/traceback.js +3 -39
  67. package/dist/renderables/align.js +2 -9
  68. package/dist/renderables/columns.d.ts +45 -29
  69. package/dist/renderables/columns.js +142 -103
  70. package/dist/renderables/constrain.js +6 -12
  71. package/dist/renderables/effect.d.ts +98 -0
  72. package/dist/renderables/effect.js +160 -0
  73. package/dist/renderables/embed.d.ts +87 -30
  74. package/dist/renderables/embed.js +134 -45
  75. package/dist/renderables/group.d.ts +6 -3
  76. package/dist/renderables/group.js +5 -0
  77. package/dist/renderables/layout.d.ts +6 -23
  78. package/dist/renderables/layout.js +24 -113
  79. package/dist/renderables/live.d.ts +40 -16
  80. package/dist/renderables/live.js +96 -83
  81. package/dist/renderables/markdown.d.ts +16 -2
  82. package/dist/renderables/markdown.js +521 -118
  83. package/dist/renderables/padding.d.ts +22 -1
  84. package/dist/renderables/padding.js +30 -24
  85. package/dist/renderables/panel.d.ts +31 -33
  86. package/dist/renderables/panel.js +137 -141
  87. package/dist/renderables/progress.d.ts +104 -16
  88. package/dist/renderables/progress.js +205 -81
  89. package/dist/renderables/progressBar.d.ts +2 -2
  90. package/dist/renderables/progressBar.js +30 -19
  91. package/dist/renderables/prompt.d.ts +37 -15
  92. package/dist/renderables/prompt.js +125 -85
  93. package/dist/renderables/ratio.d.ts +123 -0
  94. package/dist/renderables/ratio.js +213 -0
  95. package/dist/renderables/rule.d.ts +1 -0
  96. package/dist/renderables/rule.js +44 -48
  97. package/dist/renderables/spinner.d.ts +28 -2
  98. package/dist/renderables/spinner.js +52 -19
  99. package/dist/renderables/status.d.ts +13 -7
  100. package/dist/renderables/status.js +15 -29
  101. package/dist/renderables/syntax.d.ts +37 -7
  102. package/dist/renderables/syntax.js +134 -83
  103. package/dist/renderables/table.d.ts +58 -20
  104. package/dist/renderables/table.js +370 -192
  105. package/dist/renderables/traceback.d.ts +5 -4
  106. package/dist/renderables/traceback.js +239 -68
  107. package/dist/renderables/tree.d.ts +10 -2
  108. package/dist/renderables/tree.js +61 -34
  109. package/dist/renderables/viewport.js +2 -4
  110. package/dist/template-bindings/color-funcs.d.ts +31 -9
  111. package/dist/template-bindings/color-funcs.js +40 -14
  112. package/dist/template-bindings/helpers.d.ts +4 -2
  113. package/dist/template-bindings/helpers.js +5 -20
  114. package/dist/template-bindings/index.d.ts +29 -17
  115. package/dist/template-bindings/index.js +36 -20
  116. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  117. package/dist/template-bindings/palette-funcs.js +11 -7
  118. package/dist/template-bindings/style-funcs.js +4 -2
  119. package/dist/themes/buildPalette.d.ts +24 -3
  120. package/dist/themes/buildPalette.js +52 -8
  121. package/dist/themes/colorMath.d.ts +29 -17
  122. package/dist/themes/colorMath.js +74 -48
  123. package/dist/themes/colorRef.d.ts +2 -17
  124. package/dist/themes/colorRef.js +13 -31
  125. package/dist/themes/palette.d.ts +24 -0
  126. package/dist/themes/palette.js +46 -3
  127. package/dist/themes/ramp.d.ts +21 -34
  128. package/dist/themes/ramp.js +23 -42
  129. package/dist/themes/registry.js +39 -28
  130. package/dist/widgets/button.d.ts +2 -6
  131. package/dist/widgets/button.js +9 -40
  132. package/dist/widgets/checkbox.d.ts +2 -5
  133. package/dist/widgets/checkbox.js +9 -31
  134. package/dist/widgets/dropdown.d.ts +2 -5
  135. package/dist/widgets/dropdown.js +14 -48
  136. package/dist/widgets/ink.d.ts +22 -0
  137. package/dist/widgets/ink.js +31 -0
  138. package/dist/widgets/slider.d.ts +2 -5
  139. package/dist/widgets/slider.js +11 -34
  140. package/dist/widgets/text-input.d.ts +19 -10
  141. package/dist/widgets/text-input.js +156 -147
  142. package/dist/widgets/themed-widget.d.ts +20 -0
  143. package/dist/widgets/themed-widget.js +89 -0
  144. package/dist/widgets/toggle.d.ts +2 -5
  145. package/dist/widgets/toggle.js +8 -31
  146. package/dist/widgets/widget-app.d.ts +2 -0
  147. package/dist/widgets/widget-app.js +13 -6
  148. package/dist/widgets/widget-base.d.ts +2 -0
  149. package/dist/widgets/widget-base.js +9 -2
  150. package/package.json +11 -1
@@ -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,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 !== false;
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 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);
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
- if (this.maxString === undefined || value.length <= this.maxString) {
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 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) => ({
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
- 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
- })));
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, at) {
319
- const shape = this._shape(value, at.level, Infinity);
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 indentStr = " ".repeat(this.indent * at.inset);
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 = { inset: at.inset + 1, level: at.level + 1, maxWidth: at.maxWidth, column: cellLen(innerIndent), open: at.open };
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) => innerIndent + this._expandSlot(slot, i === lastSlot ? lastFrame : midFrame));
352
- return shape.open + "\n" + parts.join(EXPAND_SEPARATOR + "\n") + "\n" + indentStr + shape.close;
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
- let out = slot.head;
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
- let text;
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
- const reserve = cellLen(hole.tail) + (i === lastHole ? at.reserve : 0);
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
- text = threw(error);
786
+ laid = this._place(threw(error), here);
390
787
  }
391
- out += text + hole.tail;
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
- used += pieces.length > 0 ? cellLen(SEPARATOR) : 0;
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 + pieces.join(SEPARATOR) + shape.pad + shape.close;
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 += text + hole.tail;
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
  }