@promptctl/rich-js 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,906 @@
1
+ /**
2
+ * Pretty — formats JavaScript data structures with highlighting.
3
+ *
4
+ * Lives in `core/` beside `markup` and `emoji` because it does their job: turn
5
+ * foreign input into `RichText`. It composes no other renderable — the trait
6
+ * every file in `renderables/` shares and this one does not — and its imports
7
+ * are core primitives only. `Console.print` accepts `unknown` and must turn any
8
+ * of it into something renderable, so the formatter has to sit where `console`
9
+ * can reach it without an upward edge. [LAW:one-way-deps]
10
+ *
11
+ * Two traversals share this file, and the split is the whole design. `_format`
12
+ * lays a value out across lines; `_oneLine` answers only "does this fit on one
13
+ * line, and if so what is it?". They meet at `_shape`, which describes a
14
+ * container without committing to either layout. One traversal answering both
15
+ * questions is what made this formatter exponential: every child was rendered
16
+ * once to probe it and again to place it, so a node at depth d was visited 2^d
17
+ * times.
18
+ */
19
+ import { 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";
23
+ import { ReprHighlighter } from "./highlighter.js";
24
+ import { drawable } from "./protocol.js";
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 };
43
+ const rootFrame = (maxWidth) => ({
44
+ inset: 0,
45
+ margin: 0,
46
+ level: 0,
47
+ maxWidth,
48
+ column: 0,
49
+ reserve: 0,
50
+ hang: null,
51
+ open: new WeakSet(),
52
+ });
53
+ /**
54
+ * The column reached after `text` is appended to a line currently at `column`.
55
+ *
56
+ * A hole's formatted value can itself span multiple lines — a nested
57
+ * container that failed its own compact try. What comes after it (a Map
58
+ * entry's tail, the next hole) sits after `text`'s *last* line, not at
59
+ * `column + cellLen(text)`, so a multi-line insert resets the column to that
60
+ * last line's width instead of accumulating across lines that were never on
61
+ * the same row.
62
+ */
63
+ function lineColumn(column, text) {
64
+ const lastNewline = text.lastIndexOf("\n");
65
+ return lastNewline === -1 ? column + cellLen(text) : cellLen(text.slice(lastNewline + 1));
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
+ }
209
+ /**
210
+ * The text, when it is a single line no wider than `budget` — otherwise `null`.
211
+ *
212
+ * The one definition of "fits on one line" in this file, and it has to name
213
+ * both halves: `cellLen` scores a newline as zero cells, so a width check alone
214
+ * accepted multi-line text as compact. That is how a `Map` nested in an object
215
+ * used to render as `{ m: Map {` — a one-line object with an expansion wedged
216
+ * inside it and the closing brace back at column 0.
217
+ */
218
+ function fitOneLine(lines, budget) {
219
+ return lines.text.includes("\n") || cellLen(lines.text) > budget ? null : lines;
220
+ }
221
+ /**
222
+ * The elements of an indexed sequence, or `null` for anything that isn't one.
223
+ *
224
+ * A typed array is an array with a fixed element type, and neither of the two
225
+ * questions asked elsewhere in this file recognises it: `Array.isArray` says
226
+ * false, and its own `toString` answers the bare `1,2,3` — no brackets, nothing
227
+ * for the highlighter to colour per element. Both spellings reach the one arm
228
+ * that knows what a sequence looks like. `DataView` is excluded because it is a
229
+ * window onto bytes, not a sequence of values.
230
+ *
231
+ * Returned as `ArrayLike` rather than a materialised array because both
232
+ * spellings already are one. Converting here would box every element of a
233
+ * multi-megabyte `Buffer` to keep the first `maxLength` of them — bounding the
234
+ * output while leaving the cost of producing it unbounded.
235
+ */
236
+ function indexedElements(value) {
237
+ if (Array.isArray(value))
238
+ return value;
239
+ if (typedArrayName(value) !== undefined)
240
+ return value;
241
+ return null;
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
+ }
311
+ /**
312
+ * The first `limit` items of a sequence, pulled and no more.
313
+ *
314
+ * A `Map` or `Set` reaches its size through `.size`, which iterates nothing, so
315
+ * spreading one only ever served to throw the tail away — `print` of a
316
+ * half-million-entry cache walked all of it to show a hundred. `Infinity` is
317
+ * the unbounded case and never breaks.
318
+ */
319
+ function take(source, limit) {
320
+ const taken = [];
321
+ if (limit <= 0)
322
+ return taken;
323
+ for (const item of source) {
324
+ taken.push(item);
325
+ if (taken.length >= limit)
326
+ break;
327
+ }
328
+ return taken;
329
+ }
330
+ /**
331
+ * What a value renders as when reading it threw instead of producing it.
332
+ *
333
+ * `Pretty` reflects on data it did not create, and every reflection it performs
334
+ * — a property getter, `Object.keys`, an iterator, `toString` — can throw. The
335
+ * message travels into the output, so the failure stays visible and attributed
336
+ * to the position that produced it. [LAW:no-silent-failure] the error is
337
+ * carried rather than swallowed; a marker naming its cause is not an
338
+ * answer-shaped void, and a diagnostic that takes the program down over one
339
+ * lazily-computed field is unusable on the objects it is most wanted for.
340
+ */
341
+ function threw(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)}]`;
345
+ }
346
+ /**
347
+ * Does this value carry its own string form, or must we reflect on its keys?
348
+ *
349
+ * A `Date`, an `Error`, a `RegExp`, or any object that defines `toString`
350
+ * answers the display question itself, and reflecting on such a value throws
351
+ * that answer away — `Object.keys(new Date())` is empty, so key-reflection
352
+ * renders it `{}`. Inheriting `Object.prototype.toString` is the opposite
353
+ * signal: it yields `[object Object]`, a non-answer, so the keys are all the
354
+ * information there is.
355
+ *
356
+ * Both clauses are load-bearing. The identity check separates the two
357
+ * populations; the `typeof` check is what makes `Object.create(null)` — which
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.
362
+ */
363
+ function describesItself(value) {
364
+ const asRecord = value;
365
+ return (typeof asRecord[Symbol.toPrimitive] === "function" ||
366
+ (typeof asRecord.toString === "function" &&
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
+ }
436
+ }
437
+ /** What joins a container's positions on one line. Its width is charged for, so it is named once. */
438
+ const SEPARATOR = ", ";
439
+ /**
440
+ * What joins a container's positions across lines, once it has expanded —
441
+ * `SEPARATOR` without the trailing space a newline already provides.
442
+ * rich-pretty-xms: named, and derived rather than a second hand-typed
443
+ * literal, so the width it costs a non-last slot's own compact try —
444
+ * reserved via `Frame.reserve` — cannot drift from what `_formatObject`
445
+ * actually joins with.
446
+ */
447
+ const EXPAND_SEPARATOR = SEPARATOR.trimEnd();
448
+ /** The marker for positions the bound dropped, or nothing when it dropped none. */
449
+ const elided = (dropped) => dropped > 0 ? [{ head: `... +${dropped}`, join: "", holes: [] }] : [];
450
+ export class Pretty {
451
+ data;
452
+ indent;
453
+ expandAll;
454
+ maxLength;
455
+ maxString;
456
+ maxDepth;
457
+ indentGuides;
458
+ highlighter;
459
+ // [LAW:no-shared-mutable-globals] A private memo, written only by `_cutString`.
460
+ cutStrings = new Map();
461
+ constructor(data, options) {
462
+ this.data = data;
463
+ this.indent = options?.indent ?? 4;
464
+ this.expandAll = options?.expandAll ?? false;
465
+ this.maxLength = options?.maxLength;
466
+ this.maxString = options?.maxString;
467
+ this.maxDepth = options?.maxDepth ?? Infinity;
468
+ this.indentGuides = options?.indentGuides ?? false;
469
+ this.highlighter = options?.highlighter ?? reprHighlighter;
470
+ }
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.
474
+ yield* this.toText(options).render(options);
475
+ yield Segment.line();
476
+ }
477
+ /** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
478
+ toText(options) {
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;
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);
498
+ return text;
499
+ }
500
+ measure(_options) {
501
+ const formatted = this._format(this.data, rootFrame(_options.maxWidth)).text;
502
+ const lines = formatted.split("\n");
503
+ let max = 0;
504
+ for (const line of lines) {
505
+ max = Math.max(max, cellLen(line));
506
+ }
507
+ return { minimum: 1, maximum: Math.min(max, _options.maxWidth) };
508
+ }
509
+ /**
510
+ * The text a value shows without being reflected on, or `null` when it is an
511
+ * object and the shape arms own it.
512
+ *
513
+ * Every kind answered here renders the same wherever it sits — no
514
+ * indentation, no width to fit — which is why one method serves both
515
+ * traversals.
516
+ */
517
+ _scalar(value) {
518
+ if (value === null)
519
+ return "null";
520
+ if (value === undefined)
521
+ return "undefined";
522
+ switch (typeof value) {
523
+ case "string": {
524
+ // The dropped count is an annotation about the value, not part of it,
525
+ // so it goes after the closing quote — the way `maxLength`'s `... +N`
526
+ // sits beside the kept entries rather than inside the last one. Inside
527
+ // the quotes it reads as content, and copying it out yields a string
528
+ // the program never held.
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)
533
+ return JSON.stringify(value);
534
+ return this._cutString(value, this.maxString);
535
+ }
536
+ case "number":
537
+ case "bigint":
538
+ case "boolean":
539
+ return String(value);
540
+ case "symbol":
541
+ return value.toString();
542
+ case "function":
543
+ return `[Function: ${value.name || "anonymous"}]`;
544
+ }
545
+ return null;
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
+ }
572
+ /**
573
+ * A container, or the text standing in for one that is empty or past the
574
+ * depth cap.
575
+ *
576
+ * `positions` is a thunk because both of those answers are reachable from
577
+ * `size` alone, and reaching a position is not free: a `Map` or `Set` reaches
578
+ * its own through an iterator, so enumerating one only to elide it drains a
579
+ * container to print `Set {...}`. `size` also says how many positions were
580
+ * dropped, which is where the elision marker comes from — one derivation, so
581
+ * the count and the bound cannot disagree.
582
+ */
583
+ _container(open, close, pad, size, level, positions) {
584
+ if (size === 0)
585
+ return { kind: "text", text: open + close };
586
+ if (level >= this.maxDepth)
587
+ return { kind: "text", text: open + "..." + close };
588
+ const slots = positions();
589
+ return { kind: "container", open, close, pad, slots: [...slots, ...elided(size - slots.length)] };
590
+ }
591
+ /**
592
+ * The brackets and positions of an object, layout-free. See `Shape`.
593
+ *
594
+ * `bound` is the most positions the caller could ever use. Laying out passes
595
+ * `Infinity`, because it prints every position it is handed. A probe passes
596
+ * what is left of its line, which is already more positions than a fitting
597
+ * one could hold — `SEPARATOR` charges two cells apiece, so a container with
598
+ * more positions than `budget` overruns however narrow its contents are.
599
+ * Reaching past it only ever feeds a join that returns `null`, and reaching a
600
+ * position is what drains a `Map` or `Set`.
601
+ */
602
+ _shape(value, level, bound) {
603
+ // Clamped because a probe's budget goes negative once a line is overrun,
604
+ // and `slice` reads a negative end as counting from the far end — it would
605
+ // keep all but the last few keys where the other arms take none.
606
+ const cap = Math.max(0, Math.min(this.maxLength ?? Infinity, bound));
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]) => ({
624
+ head: "",
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
+ }
648
+ }
649
+ }
650
+ /** The laid-out form of a value, expanded across lines wherever one line will not do. */
651
+ _format(value, at) {
652
+ const scalar = this._scalar(value);
653
+ if (scalar !== null)
654
+ return this._place(scalar, at);
655
+ const object = value;
656
+ if (at.open.has(object))
657
+ return this._place("[Circular]", at);
658
+ at.open.add(object);
659
+ try {
660
+ return this._formatObject(object, at);
661
+ }
662
+ catch (error) {
663
+ // The container's *shape* would not be read — `Object.keys`, an iterator,
664
+ // `toString`. Nothing can be enumerated, so the whole container degrades;
665
+ // a value that merely would not be read degrades alone, in its own slot.
666
+ return this._place(threw(error), at);
667
+ }
668
+ finally {
669
+ at.open.delete(object);
670
+ }
671
+ }
672
+ /** The arms for a non-null object, with `value` already on the open path. */
673
+ _formatObject(value, from) {
674
+ const shape = this._shape(value, from.level, Infinity);
675
+ if (shape.kind === "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);
683
+ if (!this.expandAll) {
684
+ // rich-pretty-xms: budget from `at.column` and `at.reserve`, not from
685
+ // `at.inset` alone. `at.column` is where *this* value's text actually
686
+ // starts on its line, which already includes whatever `_expandSlot`
687
+ // prepended for us (a key and its separator, a Map entry's `" => "`,
688
+ // ...); `at.reserve` is what a fixed piece of literal text — the rest
689
+ // of this hole's own tail, or the trailing `,` a non-last slot gets
690
+ // joined with — is known to cost right after, before this value's own
691
+ // text even starts. Deriving the budget from the indent alone assumed
692
+ // nothing precedes *or follows* the value on its line, which is only
693
+ // true at the root and for the last hole of an array's last element.
694
+ const compact = this._joinOneLine(shape, {
695
+ level: at.level + 1,
696
+ budget: at.maxWidth - at.column - at.reserve,
697
+ open: at.open,
698
+ });
699
+ if (compact !== null)
700
+ return placed(cat(lead, compact), at.inset);
701
+ }
702
+ const innerIndent = at.inset + this.indent;
703
+ // Every non-last slot gets `EXPAND_SEPARATOR` appended right after it
704
+ // below, on the same line as whatever its own last character was — the
705
+ // last slot doesn't. Two frames, not one per slot: `reserve` is the only
706
+ // field that varies, and it only ever takes these two values.
707
+ const base = this._onLine({ ...at, level: at.level + 1, column: innerIndent, margin: innerIndent }, innerIndent);
708
+ const midFrame = { ...base, reserve: cellLen(EXPAND_SEPARATOR) };
709
+ const lastFrame = { ...base, reserve: 0 };
710
+ const lastSlot = shape.slots.length - 1;
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 };
742
+ }
743
+ /**
744
+ * One position, with every value in it laid out.
745
+ *
746
+ * `at.column` is where `slot.head` begins. Rather than hand-tracking a
747
+ * second, parallel "column so far" alongside `out` — two values a future
748
+ * edit could update out of step, silently reintroducing a stale-budget bug
749
+ * of the same shape this file just fixed — each hole derives its own
750
+ * column fresh from `out` via `lineColumn`, the one place "text just
751
+ * emitted" becomes "column now". [LAW:one-source-of-truth] `out` is
752
+ * already the complete record; `column` is a read of it, not a second copy
753
+ * of the same fact. `_joinOneLine`'s probe sibling tracks the analogous
754
+ * thing via `budget` shrinking instead, because a probe already discards
755
+ * anything that overruns and so never needs to know where a multi-line
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.
758
+ *
759
+ * Each hole's `reserve` is its own `tail` plus, only for the slot's last
760
+ * hole, whatever `at.reserve` already asked this whole slot to leave room
761
+ * for (`_formatObject`'s trailing `,`). An earlier hole's tail is never
762
+ * folded into a later hole's reserve — it is spent, not carried, the moment
763
+ * `out` grows past it.
764
+ */
765
+ _expandSlot(slot, at) {
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;
773
+ const lastHole = slot.holes.length - 1;
774
+ for (let i = 0; i < slot.holes.length; i++) {
775
+ const hole = slot.holes[i];
776
+ // This is the read that costs the least when it fails: neighbours are
777
+ // unaffected, so `{ a: 1, b: [Threw: …], c: 3 }` still shows everything
778
+ // that could be read.
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;
782
+ try {
783
+ laid = this._format(hole.read(), here);
784
+ }
785
+ catch (error) {
786
+ laid = this._place(threw(error), here);
787
+ }
788
+ out = follow(out, laid, hole.tail);
789
+ inset = laid.inset;
790
+ }
791
+ return out;
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
+ }
835
+ /** The one-line form of a value, or `null` when it will not fit `at.budget`. */
836
+ _oneLine(value, at) {
837
+ const scalar = this._scalar(value);
838
+ if (scalar !== null)
839
+ return fitOneLine(flat(scalar), at.budget);
840
+ const object = value;
841
+ if (at.open.has(object))
842
+ return fitOneLine(flat("[Circular]"), at.budget);
843
+ at.open.add(object);
844
+ try {
845
+ const shape = this._shape(object, at.level, at.budget);
846
+ if (shape.kind === "text")
847
+ return fitOneLine(flat(shape.text), at.budget);
848
+ return this._joinOneLine(shape, { level: at.level + 1, budget: at.budget, open: at.open });
849
+ }
850
+ catch (error) {
851
+ return fitOneLine(flat(threw(error)), at.budget);
852
+ }
853
+ finally {
854
+ at.open.delete(object);
855
+ }
856
+ }
857
+ /**
858
+ * A container's positions on one line, or `null` as soon as they overrun.
859
+ *
860
+ * The budget is spent as it goes and each position is asked for only what is
861
+ * left, so a subtree that has already outgrown the line is abandoned where it
862
+ * outgrew it rather than formatted in full and then measured.
863
+ *
864
+ * The brackets are charged before anything is read, and that ordering is what
865
+ * bounds the traversal: a budget checked only against the finished text still
866
+ * reads the whole subtree to produce text it then throws away. Charged first,
867
+ * every level costs at least the two cells of its own brackets, so a probe
868
+ * descends at most `budget / 2` levels however deep the data goes.
869
+ */
870
+ _joinOneLine(shape, at) {
871
+ let used = cellLen(shape.open) + cellLen(shape.close) + 2 * cellLen(shape.pad);
872
+ if (used > at.budget)
873
+ return null;
874
+ const pieces = [];
875
+ for (const slot of shape.slots) {
876
+ if (pieces.length > 0) {
877
+ used += cellLen(SEPARATOR);
878
+ pieces.push(flat(SEPARATOR));
879
+ }
880
+ const piece = this._slotOneLine(slot, { ...at, budget: at.budget - used });
881
+ if (piece === null)
882
+ return null;
883
+ used += cellLen(piece.text);
884
+ pieces.push(piece);
885
+ }
886
+ return cat(flat(shape.open + shape.pad), ...pieces, flat(shape.pad + shape.close));
887
+ }
888
+ /** One position on one line, or `null` when any value in it will not fit. */
889
+ _slotOneLine(slot, at) {
890
+ let out = flat(slot.head + slot.join);
891
+ for (const hole of slot.holes) {
892
+ const left = at.budget - cellLen(out.text);
893
+ let text;
894
+ try {
895
+ text = this._oneLine(hole.read(), { ...at, budget: left });
896
+ }
897
+ catch (error) {
898
+ text = fitOneLine(flat(threw(error)), left);
899
+ }
900
+ if (text === null)
901
+ return null;
902
+ out = cat(out, text, hole.tail);
903
+ }
904
+ return fitOneLine(out, at.budget);
905
+ }
906
+ }