@promptctl/rich-js 0.24.0 → 0.24.1

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 (95) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/console.js +5 -5
  9. package/dist/core/easing.d.ts +110 -10
  10. package/dist/core/easing.js +269 -36
  11. package/dist/core/json.js +2 -1
  12. package/dist/core/measure.js +2 -2
  13. package/dist/core/place.js +2 -1
  14. package/dist/core/pretty.js +2 -2
  15. package/dist/core/protocol.d.ts +73 -19
  16. package/dist/core/protocol.js +56 -15
  17. package/dist/core/render.js +2 -1
  18. package/dist/core/segment.d.ts +18 -1
  19. package/dist/core/segment.js +45 -44
  20. package/dist/core/strip.d.ts +3 -2
  21. package/dist/core/strip.js +11 -7
  22. package/dist/core/style.js +17 -6
  23. package/dist/core/text.js +22 -14
  24. package/dist/core/timeline.d.ts +84 -0
  25. package/dist/core/timeline.js +144 -0
  26. package/dist/core/tween.d.ts +49 -0
  27. package/dist/core/tween.js +69 -0
  28. package/dist/host/app.js +2 -2
  29. package/dist/index.d.ts +10 -7
  30. package/dist/index.js +13 -4
  31. package/dist/renderables/align.d.ts +3 -2
  32. package/dist/renderables/align.js +3 -0
  33. package/dist/renderables/columns.d.ts +3 -2
  34. package/dist/renderables/columns.js +7 -2
  35. package/dist/renderables/constrain.d.ts +3 -2
  36. package/dist/renderables/constrain.js +5 -1
  37. package/dist/renderables/effect.d.ts +43 -58
  38. package/dist/renderables/effect.js +58 -64
  39. package/dist/renderables/effects.d.ts +16 -14
  40. package/dist/renderables/effects.js +64 -45
  41. package/dist/renderables/embed.js +3 -2
  42. package/dist/renderables/flexStrip.d.ts +3 -2
  43. package/dist/renderables/flexStrip.js +6 -1
  44. package/dist/renderables/frames.d.ts +40 -19
  45. package/dist/renderables/frames.js +48 -21
  46. package/dist/renderables/group.d.ts +3 -2
  47. package/dist/renderables/group.js +5 -2
  48. package/dist/renderables/html-entities.d.ts +8 -0
  49. package/dist/renderables/html-entities.js +386 -0
  50. package/dist/renderables/layout.d.ts +3 -2
  51. package/dist/renderables/layout.js +15 -2
  52. package/dist/renderables/list-view.d.ts +3 -2
  53. package/dist/renderables/list-view.js +12 -5
  54. package/dist/renderables/live.js +2 -2
  55. package/dist/renderables/markdown-parse.d.ts +109 -0
  56. package/dist/renderables/markdown-parse.js +1421 -0
  57. package/dist/renderables/markdown-url.d.ts +19 -0
  58. package/dist/renderables/markdown-url.js +314 -0
  59. package/dist/renderables/markdown.d.ts +5 -7
  60. package/dist/renderables/markdown.js +356 -683
  61. package/dist/renderables/padding.d.ts +8 -20
  62. package/dist/renderables/padding.js +32 -50
  63. package/dist/renderables/panel.d.ts +26 -26
  64. package/dist/renderables/panel.js +67 -103
  65. package/dist/renderables/progress.d.ts +4 -2
  66. package/dist/renderables/progress.js +11 -2
  67. package/dist/renderables/progressBar.js +2 -2
  68. package/dist/renderables/python-space.d.ts +13 -0
  69. package/dist/renderables/python-space.js +24 -0
  70. package/dist/renderables/spinner.d.ts +26 -14
  71. package/dist/renderables/spinner.js +49 -37
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +19 -11
  74. package/dist/renderables/table.js +140 -156
  75. package/dist/renderables/traceback.d.ts +1 -1
  76. package/dist/renderables/traceback.js +6 -5
  77. package/dist/renderables/tree.d.ts +6 -5
  78. package/dist/renderables/tree.js +11 -7
  79. package/dist/renderables/viewport.d.ts +13 -5
  80. package/dist/renderables/viewport.js +41 -20
  81. package/dist/template-bindings/animation-funcs.js +2 -2
  82. package/dist/template-bindings/index.js +2 -1
  83. package/dist/themes/ramp.d.ts +18 -3
  84. package/dist/themes/ramp.js +23 -2
  85. package/dist/widgets/focus-manager.d.ts +21 -14
  86. package/dist/widgets/focus-manager.js +28 -29
  87. package/dist/widgets/index.d.ts +1 -1
  88. package/dist/widgets/static-item.d.ts +4 -3
  89. package/dist/widgets/static-item.js +9 -7
  90. package/dist/widgets/text-input.d.ts +28 -18
  91. package/dist/widgets/text-input.js +144 -101
  92. package/dist/widgets/widget-app.d.ts +19 -3
  93. package/dist/widgets/widget-app.js +39 -21
  94. package/dist/widgets/widget-base.js +0 -3
  95. package/package.json +2 -5
@@ -3,7 +3,7 @@
3
3
  * [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
4
4
  */
5
5
  import type { Segment } from "./segment.js";
6
- import { type Style, type StyleSyntaxError, type Theme } from "./style.js";
6
+ import { Style, type StyleSyntaxError, type Theme } from "./style.js";
7
7
  import { ColorDepth } from "./color.js";
8
8
  import type { Highlighter } from "./highlighter.js";
9
9
  /**
@@ -87,15 +87,21 @@ export interface RenderOptions {
87
87
  */
88
88
  onStyleError?: StyleErrorHandler;
89
89
  /**
90
- * Told of each owner that stamps its output (`core/anchor`) as it starts to
91
- * render, so the calls arrive in document order: a container's children in
92
- * the order it renders them, an owner before the owners nested in it. A
93
- * container forwards it by passing its options on, which every container
94
- * does. An owner rendered twice is told twice. A render made to measure is
95
- * not drawing and is not told (`measuring`).
90
+ * Told of every renderable `render` is asked for, as it is asked, so the
91
+ * calls arrive in document order: a container before what it holds, its
92
+ * contents in the order it renders them. `"drawn"` is a renderable drawn at
93
+ * the width it was handed; `"held"` is one handed no column, which is still
94
+ * in the view but has nothing drawn for it (`render`). A renderable rendered
95
+ * twice is told twice. A render made to measure is not drawing and is not
96
+ * told (`measuring`).
96
97
  */
97
- onDraw?: (owner: object) => void;
98
+ onPresent?: (renderable: Renderable, presence: Presence) => void;
98
99
  }
100
+ /**
101
+ * Whether a renderable the view holds was drawn, or held: in the view, handed
102
+ * no column, and so not drawn (`render`).
103
+ */
104
+ export type Presence = "drawn" | "held";
99
105
  /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
100
106
  export declare const isAscii: (text: string) => boolean;
101
107
  /**
@@ -210,7 +216,10 @@ export declare function getStyle(options: RenderOptions, style: string | Style):
210
216
  * every bundled theme. So where the depth would turn a fixed colour of the
211
217
  * pair into a slot, the pair is dropped and the run is drawn on the
212
218
  * terminal's own foreground and background, which its theme does make
213
- * readable; its attributes still set it apart. A style whose colours are
219
+ * readable; its attributes still set it apart. Both are named, not left
220
+ * unset, so the colour of whatever the run sits in — a quote, a link — does
221
+ * not show through: Rich's code keeps its own colours inside a quote, and the
222
+ * terminal's are the ones it keeps here. A style whose colours are
214
223
  * slots already (`white on blue`), or that paints no ground (`bold magenta`),
215
224
  * was written for the terminal's own colours and is drawn as written. Rich
216
225
  * draws a highlighted line number the same way below 256 colours
@@ -238,6 +247,40 @@ export declare function groundedStyle(options: RenderOptions, style: Style): Sty
238
247
  * ignored the request and emitted its full natural width.
239
248
  */
240
249
  export declare function withCellWidth(options: RenderOptions): RenderOptions;
250
+ /** Under one column there is nothing to draw, or to measure. */
251
+ export declare function drawsNothing(options: {
252
+ readonly maxWidth: number;
253
+ }): boolean;
254
+ /**
255
+ * `renderable` drawn under `options`: Rich's `Console.render`, which every
256
+ * renderable there is drawn through, so under one column nothing is.
257
+ *
258
+ * [LAW:single-enforcer] The one place that rule is kept. Every call of a
259
+ * `Renderable`'s `render` in `src/` goes through here — a container drawing
260
+ * its child, a renderable drawing the text it built, a host drawing a frame —
261
+ * and `test/seam/render-calls.test.ts` fails on one that does not. Before
262
+ * this, each container asked `drawsNothing` itself, and the ones that forgot
263
+ * drew a child handed no column anyway.
264
+ *
265
+ * It is also the one reporter of what the view holds (`onPresent`). Under one
266
+ * column a renderable is held, not drawn: what it holds (`Composite`) is
267
+ * walked and reported held in turn, and nothing renders, so no renderable
268
+ * that keeps state while it renders — a `Viewport`'s offset, a `TextInput`'s
269
+ * scroll window — ever commits it at a width that is not shown. The walk runs
270
+ * whether or not anyone listens. Drawing the squeezed subtree at one column
271
+ * instead, and dropping what it drew, did commit that state.
272
+ */
273
+ export declare function render(renderable: Renderable, options: RenderOptions): Iterable<Segment>;
274
+ /**
275
+ * `options` as a measurement is asked with. A `measure` that renders to learn
276
+ * its shape draws nothing on the frame, so it reports nothing to `onPresent` —
277
+ * one that did would put a widget in the view at the moment a container
278
+ * measured it, and keep one the frame then cropped away.
279
+ *
280
+ * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
281
+ * `withBoundedWidth` ask through it, and each `measure` passes it on.
282
+ */
283
+ export declare function measuring(options: RenderOptions): RenderOptions;
241
284
  /**
242
285
  * The options a renderable should *lay out against*: `withCellWidth`, with an
243
286
  * unbounded offer resolved to the renderable's own natural width.
@@ -273,16 +316,6 @@ export declare function withCellWidth(options: RenderOptions): RenderOptions;
273
316
  * rather than zero, and saying so names the cause — where `String.repeat` and
274
317
  * `new Array` only ever name their own argument.
275
318
  */
276
- /**
277
- * `options` as a measurement is asked with. A `measure` that renders to learn
278
- * its shape draws nothing on the frame, so it reports no owner to `onDraw` —
279
- * one that did would put a widget in document order at the moment a
280
- * container measured it, and keep one the frame then cropped away.
281
- *
282
- * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
283
- * `withBoundedWidth` ask through it, and each `measure` passes it on.
284
- */
285
- export declare function measuring(options: RenderOptions): RenderOptions;
286
319
  export declare function withBoundedWidth(options: RenderOptions, self: Measurable): RenderOptions;
287
320
  export interface Renderable {
288
321
  render(options: RenderOptions): Iterable<Segment>;
@@ -293,6 +326,26 @@ export interface Measurable {
293
326
  maximum: number;
294
327
  };
295
328
  }
329
+ /**
330
+ * A container: a renderable that draws other renderables. `contents` is what
331
+ * its `render` hands to `render`, in that order where the order is not a
332
+ * width's, decided as the render decides it — and so it is how a container
333
+ * handed no column still reports what it holds (`render`). It takes no
334
+ * options: what a container holds may depend on its state (a hidden `Layout`
335
+ * holds nothing) but never on a width, because under one column there is none.
336
+ * It commits no state, and yields only renderables: text a container builds
337
+ * from a string it holds holds nothing.
338
+ *
339
+ * [LAW:one-source-of-truth] A second description of each container's
340
+ * traversal, so `test/renderables/contents.test.ts` holds every exported
341
+ * renderable's `contents` to what its `render` reports.
342
+ *
343
+ * A consumer's container that does not implement it reports nothing it holds
344
+ * while it is handed no column.
345
+ */
346
+ export interface Composite {
347
+ contents(): Iterable<Renderable>;
348
+ }
296
349
  /**
297
350
  * What the wheel scrolls: an owner of anchors on the frame (`./anchor.ts`)
298
351
  * that moves by lines. `canScrollBy` says whether `scrollBy` with the same
@@ -305,3 +358,4 @@ export interface Scrollable {
305
358
  export declare function isRenderable(obj: unknown): obj is Renderable;
306
359
  export declare function isMeasurable(obj: unknown): obj is Measurable;
307
360
  export declare function isScrollable(obj: object): obj is Scrollable;
361
+ export declare function isComposite(obj: object): obj is Composite;
@@ -3,7 +3,7 @@
3
3
  * [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
4
4
  */
5
5
  import { cellCount } from "./cells.js";
6
- import { DEFAULT_THEME } from "./style.js";
6
+ import { DEFAULT_THEME, Style } from "./style.js";
7
7
  /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
8
8
  export const isAscii = (text) => /^[\x00-\x7F]*$/.test(text);
9
9
  export function drawable(options, chosen, ascii, glyphs = String) {
@@ -61,6 +61,8 @@ export function fitHeight(lines, height) {
61
61
  export function getStyle(options, style) {
62
62
  return (options.theme ?? DEFAULT_THEME).resolve(style);
63
63
  }
64
+ /** The terminal's own foreground on its own background. */
65
+ const TERMINAL_GROUND = Style.parse("default on default");
64
66
  /**
65
67
  * `style` as the depth the render is encoded at can draw it. A style that
66
68
  * paints its own ground in fixed colours chose its ink and ground as a pair;
@@ -69,7 +71,10 @@ export function getStyle(options, style) {
69
71
  * every bundled theme. So where the depth would turn a fixed colour of the
70
72
  * pair into a slot, the pair is dropped and the run is drawn on the
71
73
  * terminal's own foreground and background, which its theme does make
72
- * readable; its attributes still set it apart. A style whose colours are
74
+ * readable; its attributes still set it apart. Both are named, not left
75
+ * unset, so the colour of whatever the run sits in — a quote, a link — does
76
+ * not show through: Rich's code keeps its own colours inside a quote, and the
77
+ * terminal's are the ones it keeps here. A style whose colours are
73
78
  * slots already (`white on blue`), or that paints no ground (`bold magenta`),
74
79
  * was written for the terminal's own colours and is drawn as written. Rich
75
80
  * draws a highlighted line number the same way below 256 colours
@@ -79,7 +84,7 @@ export function groundedStyle(options, style) {
79
84
  const drawn = style.drawnColors(options.colorSystem ?? undefined);
80
85
  const lost = (written, shown) => written?.fixedValue !== undefined && shown?.fixedValue === undefined;
81
86
  const pairLost = style.bgcolor !== undefined && (lost(style.color, drawn.color) || lost(style.bgcolor, drawn.bgcolor));
82
- return pairLost ? style.withoutColor : style;
87
+ return pairLost ? style.add(TERMINAL_GROUND) : style;
83
88
  }
84
89
  /**
85
90
  * The options a renderable should work from: the caller's, with `maxWidth`
@@ -104,6 +109,51 @@ export function groundedStyle(options, style) {
104
109
  export function withCellWidth(options) {
105
110
  return { ...options, maxWidth: cellCount(options.maxWidth) };
106
111
  }
112
+ /** Under one column there is nothing to draw, or to measure. */
113
+ export function drawsNothing(options) {
114
+ return cellCount(options.maxWidth) < 1;
115
+ }
116
+ /**
117
+ * `renderable` drawn under `options`: Rich's `Console.render`, which every
118
+ * renderable there is drawn through, so under one column nothing is.
119
+ *
120
+ * [LAW:single-enforcer] The one place that rule is kept. Every call of a
121
+ * `Renderable`'s `render` in `src/` goes through here — a container drawing
122
+ * its child, a renderable drawing the text it built, a host drawing a frame —
123
+ * and `test/seam/render-calls.test.ts` fails on one that does not. Before
124
+ * this, each container asked `drawsNothing` itself, and the ones that forgot
125
+ * drew a child handed no column anyway.
126
+ *
127
+ * It is also the one reporter of what the view holds (`onPresent`). Under one
128
+ * column a renderable is held, not drawn: what it holds (`Composite`) is
129
+ * walked and reported held in turn, and nothing renders, so no renderable
130
+ * that keeps state while it renders — a `Viewport`'s offset, a `TextInput`'s
131
+ * scroll window — ever commits it at a width that is not shown. The walk runs
132
+ * whether or not anyone listens. Drawing the squeezed subtree at one column
133
+ * instead, and dropping what it drew, did commit that state.
134
+ */
135
+ export function render(renderable, options) {
136
+ const presence = drawsNothing(options) ? "held" : "drawn";
137
+ options.onPresent?.(renderable, presence);
138
+ if (presence === "drawn")
139
+ return renderable.render(options);
140
+ if (isComposite(renderable))
141
+ for (const content of renderable.contents())
142
+ render(content, options);
143
+ return [];
144
+ }
145
+ /**
146
+ * `options` as a measurement is asked with. A `measure` that renders to learn
147
+ * its shape draws nothing on the frame, so it reports nothing to `onPresent` —
148
+ * one that did would put a widget in the view at the moment a container
149
+ * measured it, and keep one the frame then cropped away.
150
+ *
151
+ * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
152
+ * `withBoundedWidth` ask through it, and each `measure` passes it on.
153
+ */
154
+ export function measuring(options) {
155
+ return { ...options, onPresent: undefined };
156
+ }
107
157
  /**
108
158
  * The options a renderable should *lay out against*: `withCellWidth`, with an
109
159
  * unbounded offer resolved to the renderable's own natural width.
@@ -139,18 +189,6 @@ export function withCellWidth(options) {
139
189
  * rather than zero, and saying so names the cause — where `String.repeat` and
140
190
  * `new Array` only ever name their own argument.
141
191
  */
142
- /**
143
- * `options` as a measurement is asked with. A `measure` that renders to learn
144
- * its shape draws nothing on the frame, so it reports no owner to `onDraw` —
145
- * one that did would put a widget in document order at the moment a
146
- * container measured it, and keep one the frame then cropped away.
147
- *
148
- * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
149
- * `withBoundedWidth` ask through it, and each `measure` passes it on.
150
- */
151
- export function measuring(options) {
152
- return { ...options, onDraw: undefined };
153
- }
154
192
  export function withBoundedWidth(options, self) {
155
193
  const parsed = withCellWidth(options);
156
194
  if (Number.isFinite(parsed.maxWidth))
@@ -182,3 +220,6 @@ export function isScrollable(obj) {
182
220
  "scrollBy" in obj &&
183
221
  typeof obj.scrollBy === "function");
184
222
  }
223
+ export function isComposite(obj) {
224
+ return "contents" in obj && typeof obj.contents === "function";
225
+ }
@@ -42,6 +42,7 @@
42
42
  * `Style.render` returns bare text for a null colour system.
43
43
  */
44
44
  import { ColorDepth, resolveDestination } from "./color.js";
45
+ import { render } from "./protocol.js";
45
46
  import { OSC8_CLOSE, osc8Open } from "./osc8.js";
46
47
  const DEFAULT_WIDTH = 80;
47
48
  /**
@@ -101,5 +102,5 @@ export function renderToString(renderable, options) {
101
102
  // so a renderable measures what this very call will draw.
102
103
  colorSystem: destination.colorSystem,
103
104
  };
104
- return segmentsToString(renderable.render(renderOptions), destination);
105
+ return segmentsToString(render(renderable, renderOptions), destination);
105
106
  }
@@ -42,6 +42,15 @@ export declare class Segment {
42
42
  * whatever drew it, and its anchor says so; see `./anchor.ts`.
43
43
  */
44
44
  splitCells(position: CellCol): [Segment, Segment];
45
+ /**
46
+ * Cuts at each of `positions`, non-decreasing and inside this segment's
47
+ * cells: one piece more than there are positions, each exactly as wide as
48
+ * the distance between its cuts, the way `splitCells` cuts at one.
49
+ *
50
+ * [LAW:single-enforcer] Each piece but the first starts that many cells
51
+ * further into whatever drew it, and its anchor says so; see `./anchor.ts`.
52
+ */
53
+ private cutAt;
45
54
  private static _line;
46
55
  static line(): Segment;
47
56
  /**
@@ -150,7 +159,15 @@ export declare class Segment {
150
159
  */
151
160
  static removeColor(segments: Iterable<Segment>): Iterable<Segment>;
152
161
  /**
153
- * Divides segments at cell positions. Yields arrays of segments for each section.
162
+ * Divides segments at cell positions: a section ending at each cut, and the
163
+ * cells after the last cut when there are any. A segment wider than zero
164
+ * that starts at a cut opens the section after it; one with no cells stays
165
+ * in the section it ends.
166
+ *
167
+ * One walk, and each segment is cut once at every cut that falls inside it:
168
+ * cutting what is left of it again at each cut re-measures and copies the
169
+ * rest every time, so a long line wrapped into many rows took time quadratic
170
+ * in its length.
154
171
  */
155
172
  static divide(segments: Segment[], cuts: number[]): Segment[][];
156
173
  /**
@@ -59,11 +59,10 @@ export class Segment {
59
59
  return [this, new Segment("")];
60
60
  if (position <= 0)
61
61
  return [new Segment(""), this];
62
- const [leftText, rightText] = cutCells(this.text, position);
63
- return [
64
- new Segment(leftText, this.style),
65
- new Segment(rightText, this.style?.shiftedBy(position)),
66
- ];
62
+ return this.cutAt([position]);
63
+ }
64
+ cutAt(positions) {
65
+ return cutCells(this.text, positions).map((text, piece) => new Segment(text, piece === 0 ? this.style : this.style?.shiftedBy(positions[piece - 1])));
67
66
  }
68
67
  // --- Static factories ---
69
68
  static _line;
@@ -129,8 +128,11 @@ export class Segment {
129
128
  */
130
129
  static *applyStyle(segments, style, postStyle) {
131
130
  // Applying no style is the identity, so the segments pass through as they
132
- // are instead of each being copied into an equal one.
133
- if (!style && !postStyle) {
131
+ // are instead of each being copied into an equal one. A null style — a
132
+ // theme's plain entry, such as `markdown.item` — is no style.
133
+ const pre = style?.isNull ? undefined : style;
134
+ const post = postStyle?.isNull ? undefined : postStyle;
135
+ if (!pre && !post) {
134
136
  yield* segments;
135
137
  return;
136
138
  }
@@ -140,10 +142,10 @@ export class Segment {
140
142
  continue;
141
143
  }
142
144
  let s = segment.style;
143
- if (style)
144
- s = style.add(s);
145
- if (postStyle)
146
- s = s ? s.add(postStyle) : postStyle;
145
+ if (pre)
146
+ s = pre.add(s);
147
+ if (post)
148
+ s = s ? s.add(post) : post;
147
149
  yield new Segment(segment.text, s, segment.control);
148
150
  }
149
151
  }
@@ -398,45 +400,44 @@ export class Segment {
398
400
  }
399
401
  }
400
402
  /**
401
- * Divides segments at cell positions. Yields arrays of segments for each section.
403
+ * Divides segments at cell positions: a section ending at each cut, and the
404
+ * cells after the last cut when there are any. A segment wider than zero
405
+ * that starts at a cut opens the section after it; one with no cells stays
406
+ * in the section it ends.
407
+ *
408
+ * One walk, and each segment is cut once at every cut that falls inside it:
409
+ * cutting what is left of it again at each cut re-measures and copies the
410
+ * rest every time, so a long line wrapped into many rows took time quadratic
411
+ * in its length.
402
412
  */
403
413
  static divide(segments, cuts) {
404
414
  if (cuts.length === 0)
405
415
  return [segments];
406
- const result = [];
407
- let segmentIndex = 0;
408
- let cellOffset = 0;
409
- let currentSegment = segments[segmentIndex];
410
- for (const cut of cuts) {
411
- const section = [];
412
- while (currentSegment && cellOffset + currentSegment.cellLength <= cut) {
413
- section.push(currentSegment);
414
- cellOffset += currentSegment.cellLength;
415
- segmentIndex++;
416
- currentSegment = segments[segmentIndex];
417
- }
418
- if (currentSegment && !currentSegment.isControl && cellOffset < cut) {
419
- const splitAt = asCellCol(cut - cellOffset);
420
- const [left, right] = currentSegment.splitCells(splitAt);
421
- if (left.hasText)
422
- section.push(left);
423
- cellOffset += left.cellLength;
424
- currentSegment = right;
416
+ const sections = [[]];
417
+ let offset = 0;
418
+ let next = 0;
419
+ for (const segment of segments) {
420
+ const end = offset + segment.cellLength;
421
+ const inside = [];
422
+ for (; next < cuts.length && cuts[next] < end; next++) {
423
+ const cut = cuts[next];
424
+ if (cut <= offset)
425
+ sections.push([]);
426
+ else
427
+ inside.push(asCellCol(cut - offset));
425
428
  }
426
- result.push(section);
427
- }
428
- // Remaining segments
429
- const tail = [];
430
- if (currentSegment?.hasText)
431
- tail.push(currentSegment);
432
- segmentIndex++;
433
- while (segmentIndex < segments.length) {
434
- tail.push(segments[segmentIndex]);
435
- segmentIndex++;
429
+ const [head, ...rest] = inside.length === 0 ? [segment] : segment.cutAt(inside);
430
+ sections[sections.length - 1].push(head);
431
+ // A piece between two cuts at one cell has no text: the section it is holds nothing.
432
+ for (const piece of rest)
433
+ sections.push(piece.hasText ? [piece] : []);
434
+ offset = end;
436
435
  }
437
- if (tail.length > 0)
438
- result.push(tail);
439
- return result;
436
+ for (; next < cuts.length; next++)
437
+ sections.push([]);
438
+ if (sections[sections.length - 1].length === 0)
439
+ sections.pop();
440
+ return sections;
440
441
  }
441
442
  // --- Layout helpers ---
442
443
  /**
@@ -28,7 +28,7 @@
28
28
  import type { Anchor } from "./anchor.js";
29
29
  import { Segment } from "./segment.js";
30
30
  import { Style } from "./style.js";
31
- import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
31
+ import { type Composite, type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
32
32
  /**
33
33
  * Items in a `Strip` expose their per-edge style so joiners can paint the
34
34
  * transition glyph at each boundary.
@@ -97,11 +97,12 @@ export declare function drawJoin<T extends StyledRenderable>(joiner: Joiner<T>,
97
97
  * item.
98
98
  */
99
99
  export declare function drawItem(item: StyledRenderable): Renderable;
100
- export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable, Measurable {
100
+ export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable, Measurable, Composite {
101
101
  readonly items: readonly T[];
102
102
  readonly joiner: Joiner<T>;
103
103
  constructor(items: readonly T[], joiner: Joiner<T>);
104
104
  render(options: RenderOptions): Iterable<Segment>;
105
+ contents(): Iterable<Renderable>;
105
106
  /**
106
107
  * [LAW:one-source-of-truth] The width of the one row it draws, and that is
107
108
  * both bounds: the row never wraps and no item is offered less than the whole
@@ -31,7 +31,7 @@ import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
31
31
  import { Oklch } from "./oklch.js";
32
32
  import { cellLen } from "./cells.js";
33
33
  import { fnv1a } from "./fnv1a.js";
34
- import { drawable, getStyle, isAscii } from "./protocol.js";
34
+ import { drawable, getStyle, isAscii, render } from "./protocol.js";
35
35
  /**
36
36
  * The owner every cell a strip's joiner draws is stamped with
37
37
  * (`Segment.anchorLines`), whatever the joiner: a seam is told by what drew
@@ -80,7 +80,7 @@ export function colorsOwnedBy(anchor, owners) {
80
80
  */
81
81
  export function drawJoin(joiner, left, right, context) {
82
82
  const join = joiner.join(left, right, context);
83
- return { render: (options) => Segment.anchorLines([[...join.render(options)]], JOIN)[0] };
83
+ return { render: (options) => Segment.anchorLines([[...render(join, options)]], JOIN)[0] };
84
84
  }
85
85
  /**
86
86
  * [LAW:single-enforcer] What `item` draws, every cell stamped as the item's,
@@ -89,7 +89,7 @@ export function drawJoin(joiner, left, right, context) {
89
89
  * item.
90
90
  */
91
91
  export function drawItem(item) {
92
- return { render: (options) => Segment.anchorLines([[...item.render(options)]], item)[0] };
92
+ return { render: (options) => Segment.anchorLines([[...render(item, options)]], item)[0] };
93
93
  }
94
94
  // --- Strip ---
95
95
  export class Strip {
@@ -112,12 +112,12 @@ export class Strip {
112
112
  // `left` fills the canvas, and the first item would pad over the rest of
113
113
  // the row. Where the row sits in the width is its container's to say.
114
114
  const row = { ...options, justify: undefined };
115
- yield* drawJoin(this.joiner, null, items[0], { index: 0 }).render(row);
115
+ yield* render(drawJoin(this.joiner, null, items[0], { index: 0 }), row);
116
116
  for (let i = 0; i < items.length; i++) {
117
117
  const item = items[i];
118
- yield* drawItem(item).render(row);
118
+ yield* render(drawItem(item), row);
119
119
  const next = i + 1 < items.length ? items[i + 1] : null;
120
- yield* drawJoin(this.joiner, item, next, { index: i + 1 }).render(row);
120
+ yield* render(drawJoin(this.joiner, item, next, { index: i + 1 }), row);
121
121
  }
122
122
  // [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
123
123
  // it emits, including the last, and the other block-level renderables
@@ -131,6 +131,10 @@ export class Strip {
131
131
  // of which renderables end their own line and which are fragments.
132
132
  yield Segment.line();
133
133
  }
134
+ // The items; a join is drawn from what the joiner emits for it, not held.
135
+ contents() {
136
+ return this.items;
137
+ }
134
138
  /**
135
139
  * [LAW:one-source-of-truth] The width of the one row it draws, and that is
136
140
  * both bounds: the row never wraps and no item is offered less than the whole
@@ -140,7 +144,7 @@ export class Strip {
140
144
  */
141
145
  measure(options) {
142
146
  let width = 0;
143
- for (const segment of this.render(options))
147
+ for (const segment of render(this, options))
144
148
  width += segment.cellLength;
145
149
  return { minimum: width, maximum: width };
146
150
  }
@@ -129,7 +129,8 @@ export class Style {
129
129
  this.frame = options.frame;
130
130
  this.encircle = options.encircle;
131
131
  this.overline = options.overline;
132
- this.link = options.link;
132
+ // As the reference's `self._link`, read only for truth: an empty URL is no link.
133
+ this.link = options.link || undefined;
133
134
  this.meta = options.meta;
134
135
  this.anchor = options.anchor;
135
136
  }
@@ -695,16 +696,26 @@ export const DEFAULT_STYLES = {
695
696
  "syntax.line_number.highlight": Style.parse("bold grey93 on grey27"),
696
697
  // Markdown
697
698
  "markdown.h1": Style.parse("bold underline"),
698
- "markdown.h2": Style.parse("bold"),
699
- "markdown.h3": Style.parse("bold dim"),
700
- "markdown.h4": Style.parse("bold dim italic"),
699
+ "markdown.h2": Style.parse("underline magenta"),
700
+ "markdown.h3": Style.parse("bold magenta"),
701
+ "markdown.h4": Style.parse("italic magenta"),
702
+ "markdown.h5": Style.parse("italic"),
703
+ "markdown.h6": Style.parse("dim"),
701
704
  // A fixed cyan, not the theme's: `cyan` is dark enough on some themes to
702
705
  // sink into grey11. Bold, as Rich's is: at 16 colours a renderable draws it
703
706
  // without its pair (`groundedStyle`), and bold is what still marks code.
704
707
  "markdown.code": Style.parse("bold dark_turquoise on grey11"),
705
- "markdown.hr": Style.parse("yellow"),
708
+ "markdown.em": Style.parse("italic"),
709
+ "markdown.strong": Style.parse("bold"),
710
+ "markdown.s": Style.parse("strike"),
711
+ "markdown.kbd": Style.parse("bold bright_yellow"),
712
+ "markdown.block_quote": Style.parse("magenta"),
713
+ "markdown.hr": Style.parse("dim"),
714
+ "markdown.item": NULL_STYLE,
715
+ "markdown.item.bullet": Style.parse("bold"),
716
+ "markdown.item.number": Style.parse("cyan"),
706
717
  "markdown.link": Style.parse("bright_blue"),
707
- "markdown.link_url": Style.parse("blue"),
718
+ "markdown.link_url": Style.parse("underline blue"),
708
719
  "markdown.table.border": Style.parse("cyan"),
709
720
  "markdown.table.header": Style.parse("not bold cyan"),
710
721
  // Progress
package/dist/core/text.js CHANGED
@@ -9,10 +9,20 @@ import { stripOscTerminators } from "./osc8.js";
9
9
  import { drawable, getStyle, withBoundedWidth } from "./protocol.js";
10
10
  // Every C0 control and DEL except \t and \n. Rich's `Text` strips only
11
11
  // `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR); the rest of the set is the
12
- // port's extension, so no C0 byte of content can start an escape sequence or
13
- // move the cursor on the wire. C1 controls (U+0080–U+009F) pass, as in Rich.
14
- // [LAW:single-enforcer] Single place where control chars are sanitized
15
- const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
12
+ // port's extension, so no byte of content can start an escape sequence or move
13
+ // the cursor on the wire: the C1 controls (U+0080–U+009F) go as well, since a
14
+ // terminal reads U+009B as CSI and U+009D as OSC (TextInput refuses them for
15
+ // the same reason). So do the format controls that hold state past the text,
16
+ // also the port's extension: the bidi embeddings and overrides with their
17
+ // closer PDF (U+202A–U+202E), the isolates with their closer PDI
18
+ // (U+2066–U+2069), and the deprecated shaping and swapping switches
19
+ // (U+206A–U+206F). An opener left unclosed in content reverses whatever is
20
+ // drawn after it on the line, and a closer whose opener is gone closes
21
+ // nothing of the text's own. The bidi marks (LRM, RLM, ALM) hold no state and
22
+ // pass, since right-to-left text needs them.
23
+ // [LAW:single-enforcer] Single place where control chars are sanitized: every
24
+ // string that becomes part of a Text's characters passes here.
25
+ const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u206F]/g;
16
26
  export function stripControlChars(text) {
17
27
  return text.replace(CONTROL_CHARS_RE, "");
18
28
  }
@@ -473,10 +483,9 @@ export class RichText {
473
483
  return this;
474
484
  }
475
485
  pad(count, char) {
476
- const c = char ?? " ";
477
- const padding = c.repeat(count);
478
- // Shift all spans to the right
479
- this._spans = this._spans.map((s) => s.move(count));
486
+ const padding = stripControlChars(char ?? " ").repeat(count);
487
+ // Shift all spans right by what the left side gained.
488
+ this._spans = this._spans.map((s) => s.move(padding.length));
480
489
  this._text = padding + this._text + padding;
481
490
  return this;
482
491
  }
@@ -492,14 +501,13 @@ export class RichText {
492
501
  return this;
493
502
  }
494
503
  padLeft(count, char) {
495
- const c = char ?? " ";
496
- this._spans = this._spans.map((s) => s.move(count));
497
- this._text = c.repeat(count) + this._text;
504
+ const padding = stripControlChars(char ?? " ").repeat(count);
505
+ this._spans = this._spans.map((s) => s.move(padding.length));
506
+ this._text = padding + this._text;
498
507
  return this;
499
508
  }
500
509
  padRight(count, char) {
501
- const c = char ?? " ";
502
- this._text += c.repeat(count);
510
+ this._text += stripControlChars(char ?? " ").repeat(count);
503
511
  return this;
504
512
  }
505
513
  setLength(length) {
@@ -558,7 +566,7 @@ export class RichText {
558
566
  // value, so width 0 and a marker wider than the width run the same walk
559
567
  // as every other call: what is drawn plus what is kept is `cells` wide.
560
568
  const cells = cellCount(width);
561
- const marker = cellFit(options?.marker ?? "\u2026", cells);
569
+ const marker = cellFit(stripControlChars(options?.marker ?? "\u2026"), cells);
562
570
  const budget = asCellCol(cells - cellLen(marker));
563
571
  if (mode === "right") {
564
572
  this._cropRightTo(budget);