@promptctl/rich-js 0.24.0 → 0.25.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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -21,6 +21,8 @@ import { cellLen } from "./cells.js";
21
21
  import { Style, StyleSyntaxError } from "./style.js";
22
22
  import { RichText, stripControlChars } from "./text.js";
23
23
  import { emojiReplace } from "./emoji.js";
24
+ import { pattern } from "./pattern.js";
25
+ import { State } from "./state.js";
24
26
  // --- Tag ---
25
27
  export class Tag {
26
28
  name;
@@ -159,7 +161,7 @@ function excerpt(text, index) {
159
161
  */
160
162
  export function escape(text) {
161
163
  return text
162
- .replace(ESCAPE_RE, (_, run, live) => live === undefined ? `${run}\\[` : `${run}${run}\\[`)
164
+ .replace(new RegExp(ESCAPE_RE.source, "g"), (_, run, live) => live === undefined ? `${run}\\[` : `${run}${run}\\[`)
163
165
  .replace(TRAILING_RUN_RE, "$&$&");
164
166
  }
165
167
  // --- render ---
@@ -188,18 +190,18 @@ export function escape(text) {
188
190
  // run, on exactly the untrusted text `escape` is for.
189
191
  const TAG_BODY = String.raw `[a-z#/@][^[]*?\]`;
190
192
  const RUN = String.raw `(?<!\\)(\\*)`;
191
- const TAG_RE = new RegExp(String.raw `${RUN}(\[${TAG_BODY})`, "g");
193
+ const TAG_RE = pattern(new RegExp(String.raw `${RUN}(\[${TAG_BODY})`));
192
194
  /**
193
195
  * Every bracket and the run in front of it; the empty second group is set when
194
196
  * a tag starts there or could, once more text is joined after the end. It is an
195
197
  * alternation rather than `(…)?` because a quantified group may not match empty.
196
198
  */
197
- const ESCAPE_RE = new RegExp(String.raw `${RUN}\[(?:(?=${TAG_BODY}|(?:[a-z#/@][^[\]]*)?$)()|)`, "g");
198
- const TRAILING_RUN_RE = /(?<!\\)\\+$/;
199
+ const ESCAPE_RE = pattern(new RegExp(String.raw `${RUN}\[(?:(?=${TAG_BODY}|(?:[a-z#/@][^[\]]*)?$)()|)`));
200
+ const TRAILING_RUN_RE = pattern(/(?<!\\)\\+$/);
199
201
  function tokenize(markup) {
200
202
  const tokens = [];
201
203
  let position = 0;
202
- for (const match of markup.matchAll(TAG_RE)) {
204
+ for (const match of markup.matchAll(new RegExp(TAG_RE.source, "g"))) {
203
205
  const run = match[1];
204
206
  const captured = match[2];
205
207
  let start = match.index;
@@ -292,9 +294,9 @@ function findLastIndex(items, match) {
292
294
  // could never fire. This tracked `[A-Za-z]` while `TAG_RE` wrongly admitted
293
295
  // uppercase, which is the same drift, one subset down (rich-markup-sec).
294
296
  const PLUGIN_TAG_NAME_SRC = "[a-z][A-Za-z0-9_-]*";
295
- const LEGAL_PLUGIN_TAG_NAME = new RegExp(`^${PLUGIN_TAG_NAME_SRC}$`);
297
+ const LEGAL_PLUGIN_TAG_NAME = pattern(new RegExp(`^${PLUGIN_TAG_NAME_SRC}$`));
296
298
  export class MarkupRegistry {
297
- _handlers = new Map();
299
+ _handlers = new State(new Map());
298
300
  register(name, handler) {
299
301
  // [LAW:no-silent-failure] Registration used to accept any string, including
300
302
  // names markup can never address (`a.b` stops the tag scan at the dot), and
@@ -305,16 +307,18 @@ export class MarkupRegistry {
305
307
  if (isReservedTagName(name)) {
306
308
  throw new MarkupError(`Cannot register markup tag "${name}": name is reserved by a built-in style.`);
307
309
  }
308
- this._handlers.set(name, handler);
310
+ this._handlers.set(new Map(this._handlers.get()).set(name, handler));
309
311
  }
310
312
  unregister(name) {
311
- this._handlers.delete(name);
313
+ const handlers = new Map(this._handlers.get());
314
+ handlers.delete(name);
315
+ this._handlers.set(handlers);
312
316
  }
313
317
  has(name) {
314
- return this._handlers.has(name);
318
+ return this._handlers.get().has(name);
315
319
  }
316
320
  get(name) {
317
- return this._handlers.get(name);
321
+ return this._handlers.get().get(name);
318
322
  }
319
323
  }
320
324
  // [LAW:one-source-of-truth] Reserved-name detection delegates to Style.parse —
@@ -356,7 +360,7 @@ function isSpace(c) {
356
360
  // continues with the whitespace that introduces attributes. Anything else —
357
361
  // `[table.header]`, `[table=x]` — is the built-in dialect's, and falls through
358
362
  // exactly as an unregistered name would.
359
- const PLUGIN_TAG_HEAD = new RegExp(`^(${PLUGIN_TAG_NAME_SRC})(?=$|[ \\t])`);
363
+ const PLUGIN_TAG_HEAD = pattern(new RegExp(`^(${PLUGIN_TAG_NAME_SRC})(?=$|[ \\t])`));
360
364
  function parsePluginAttrs(after) {
361
365
  // Hand-written tokenizer. Walks `key=value` pairs separated by whitespace;
362
366
  // values may be bare, single-quoted, or double-quoted. Matches the syntax
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Measurement — min/max cell width calculation for renderables.
3
3
  */
4
- import { isMeasurable, measuring } from "./protocol.js";
4
+ import { drawsNothing, isMeasurable, measure } from "./protocol.js";
5
5
  export class Measurement {
6
6
  minimum;
7
7
  maximum;
@@ -45,11 +45,11 @@ export class Measurement {
45
45
  // `Measurement.get` answers for it — so under an unbounded offer it asks for
46
46
  // `Infinity`, and `withBoundedWidth` refuses rather than guessing a width.
47
47
  static get(options, renderable) {
48
- if (options.maxWidth < 1)
48
+ if (drawsNothing(options))
49
49
  return new Measurement(0, 0);
50
50
  if (!isMeasurable(renderable))
51
51
  return new Measurement(0, options.maxWidth);
52
- const { minimum, maximum } = renderable.measure(measuring(options));
52
+ const { minimum, maximum } = measure(renderable, options);
53
53
  return new Measurement(minimum, Math.min(maximum, options.maxWidth)).normalize();
54
54
  }
55
55
  }
@@ -12,8 +12,17 @@ export declare const MEMO_MAX = 4096;
12
12
  * be a pure function of `key`, so a value recomputed after a clear is the one
13
13
  * that was dropped.
14
14
  */
15
- export declare class Memo<V extends NonNullable<unknown>> {
15
+ export declare class Memo<V extends NonNullable<unknown>, K extends string = string> {
16
16
  private readonly entries;
17
17
  get size(): number;
18
- get(key: string, compute: (key: string) => V): V;
18
+ get(key: K, compute: (key: K) => V): V;
19
+ }
20
+ /**
21
+ * `Memo`'s contract for a key that is an object rather than a string: a
22
+ * value computed once per key, for as long as the key lives. Held weakly, so
23
+ * it needs no bound — a key a host stops holding takes its entry with it.
24
+ */
25
+ export declare class WeakMemo<V extends NonNullable<unknown>, K extends object> {
26
+ private readonly entries;
27
+ get(key: K, compute: (key: K) => V): V;
19
28
  }
package/dist/core/memo.js CHANGED
@@ -32,3 +32,19 @@ export class Memo {
32
32
  return value;
33
33
  }
34
34
  }
35
+ /**
36
+ * `Memo`'s contract for a key that is an object rather than a string: a
37
+ * value computed once per key, for as long as the key lives. Held weakly, so
38
+ * it needs no bound — a key a host stops holding takes its entry with it.
39
+ */
40
+ export class WeakMemo {
41
+ entries = new WeakMap();
42
+ get(key, compute) {
43
+ const remembered = this.entries.get(key);
44
+ if (remembered !== undefined)
45
+ return remembered;
46
+ const value = compute(key);
47
+ this.entries.set(key, value);
48
+ return value;
49
+ }
50
+ }
@@ -96,6 +96,11 @@ function inGamut(r, g, b) {
96
96
  g >= -GAMUT_EPS && g <= 1 + GAMUT_EPS &&
97
97
  b >= -GAMUT_EPS && b <= 1 + GAMUT_EPS);
98
98
  }
99
+ /** Polar (C, h°) → OKLab (a, b): the one spelling `deltaE` measures by and `toRgba` draws by. */
100
+ function labOf(c, h) {
101
+ const hRad = h * (Math.PI / 180);
102
+ return [c * Math.cos(hRad), c * Math.sin(hRad)];
103
+ }
99
104
  // ---------------------------------------------------------------------------
100
105
  // Oklch — the value type.
101
106
  // ---------------------------------------------------------------------------
@@ -130,6 +135,7 @@ export class Oklch {
130
135
  if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
131
136
  throw new RangeError(`Oklch.alpha must be a finite number in [0,1]; got ${alpha}`);
132
137
  }
138
+ this.#lab = labOf(c, h);
133
139
  }
134
140
  /** sRGB → linear → OKLab → polar. Pure; no I/O, no allocation beyond the result. */
135
141
  static fromRgba(color) {
@@ -265,16 +271,21 @@ export class Oklch {
265
271
  * Symmetric and pure.
266
272
  */
267
273
  deltaE(other) {
268
- const a = (this.h * Math.PI) / 180;
269
- const b = (other.h * Math.PI) / 180;
270
- return Math.hypot(this.l - other.l, this.c * Math.cos(a) - other.c * Math.cos(b), this.c * Math.sin(a) - other.c * Math.sin(b));
274
+ const [a, b] = this.#lab;
275
+ const [otherA, otherB] = other.#lab;
276
+ return Math.hypot(this.l - other.l, a - otherA, b - otherB);
271
277
  }
278
+ /**
279
+ * This colour's OKLab a and b, kept with it: a colour matched against a
280
+ * palette is measured against every entry, and each entry against every
281
+ * colour it is matched with, so the trigonometry is paid once a colour
282
+ * rather than once a comparison.
283
+ */
284
+ #lab;
272
285
  /** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
273
286
  * already-normalized values so this never sees out-of-range inputs. */
274
287
  toLinearRgb(l, C, h) {
275
- const hRad = h * (Math.PI / 180);
276
- const aLab = C * Math.cos(hRad);
277
- const bLab = C * Math.sin(hRad);
288
+ const [aLab, bLab] = labOf(C, h);
278
289
  const lCube = l + 0.3963377774 * aLab + 0.2158037573 * bLab;
279
290
  const mCube = l - 0.1055613458 * aLab - 0.0638541728 * bLab;
280
291
  const sCube = l - 0.0894841775 * aLab - 1.2914855480 * bLab;
@@ -49,7 +49,7 @@ export declare const OSC8_CLOSE = "\u001B]8;;\u001B\\";
49
49
  * on a close. To read bytes back use `osc8Sequences`; this pattern is exported
50
50
  * for composing into a larger one (a width measure's zero-width alternation).
51
51
  */
52
- export declare const OSC8: RegExp;
52
+ export declare const OSC8: import("./pattern.js").Pattern;
53
53
  /** One OSC 8 sequence found in rendered bytes. A close has `uri === ""`. */
54
54
  export interface Osc8Sequence {
55
55
  /** Offset of the sequence in the scanned string. */
package/dist/core/osc8.js CHANGED
@@ -31,15 +31,15 @@
31
31
  * exact, not approximate.
32
32
  */
33
33
  import { fnv1a } from "./fnv1a.js";
34
+ import { pattern } from "./pattern.js";
34
35
  /**
35
36
  * The bytes that prematurely terminate an OSC 8 sequence: ESC (`\x1b`, which
36
37
  * begins ST `ESC \`), BEL (`\x07`), and the 8-bit ST (`\x9c`).
37
38
  */
38
39
  const TERMINATORS = "\\x1b\\x07\\x9c";
39
- const TERMINATOR_RE = new RegExp(`[${TERMINATORS}]`, "g");
40
40
  /** Remove every byte that could break a URL out of its OSC 8 wrap. */
41
41
  export function stripOscTerminators(url) {
42
- return url.replace(TERMINATOR_RE, "");
42
+ return url.replace(new RegExp(`[${TERMINATORS}]`, "g"), "");
43
43
  }
44
44
  /**
45
45
  * The bytes that open a hyperlink to `url`.
@@ -62,14 +62,13 @@ export const OSC8_CLOSE = "\x1b]8;;\x1b\\";
62
62
  * on a close. To read bytes back use `osc8Sequences`; this pattern is exported
63
63
  * for composing into a larger one (a width measure's zero-width alternation).
64
64
  */
65
- export const OSC8 = new RegExp(`\\x1b\\]8;([^;${TERMINATORS}]*);([^${TERMINATORS}]*)(?:\\x1b\\\\|\\x07|\\x9c)`);
66
- const OSC8_SCAN = new RegExp(OSC8.source, "g");
65
+ export const OSC8 = pattern(new RegExp(`\\x1b\\]8;([^;${TERMINATORS}]*);([^${TERMINATORS}]*)(?:\\x1b\\\\|\\x07|\\x9c)`));
67
66
  /**
68
67
  * Every OSC 8 sequence in `text`, in order — the typed way to read rendered
69
68
  * bytes back.
70
69
  */
71
70
  export function osc8Sequences(text) {
72
- return [...text.matchAll(OSC8_SCAN)].map((m) => ({
71
+ return [...text.matchAll(new RegExp(OSC8.source, "g"))].map((m) => ({
73
72
  index: m.index,
74
73
  length: m[0].length,
75
74
  params: m[1],
@@ -71,7 +71,8 @@ export declare class SurfacePainter implements Painter {
71
71
  * Paint `frame` over the last frame, leaving the cursor on its last row,
72
72
  * and return it as painted. No row is wider than the screen, since one that
73
73
  * soft-wrapped would push every row below it down a row the next paint does
74
- * not go back over.
74
+ * not go back over. The rows returned are what `destination` shows
75
+ * (`shownOn`): under reduced motion, nothing in them blinks.
75
76
  */
76
77
  paint(frame: FrameSource, screen: Screen, destination: Destination): Segment[][];
77
78
  /**
@@ -13,7 +13,7 @@
13
13
  * the last row, and the newline that ends it is written once, when the frame
14
14
  * is handed back.
15
15
  */
16
- import { segmentsToString } from "./render.js";
16
+ import { segmentsToString, shownOn } from "./render.js";
17
17
  import { Segment } from "./segment.js";
18
18
  const GEOMETRY = {
19
19
  alternate: {
@@ -84,10 +84,11 @@ export class SurfacePainter {
84
84
  * Paint `frame` over the last frame, leaving the cursor on its last row,
85
85
  * and return it as painted. No row is wider than the screen, since one that
86
86
  * soft-wrapped would push every row below it down a row the next paint does
87
- * not go back over.
87
+ * not go back over. The rows returned are what `destination` shows
88
+ * (`shownOn`): under reduced motion, nothing in them blinks.
88
89
  */
89
90
  paint(frame, screen, destination) {
90
- const rows = fitRows(frame(), screen);
91
+ const rows = fitRows(frame(), screen).map((row) => row.map((segment) => shownOn(segment, destination)));
91
92
  this.write(SYNC_START + this.over(rows, screen, destination) + SYNC_END);
92
93
  return rows;
93
94
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * pattern — `Pattern`: a regular expression held where every caller reaches
3
+ * it — module scope, a class static, a renderable's field — with no cursor
4
+ * for them to share.
5
+ *
6
+ * A `RegExp` carries `lastIndex`, and the `exec`, `test` and `replace` of a
7
+ * global or sticky one write it, so a global regex held at module scope is
8
+ * state that every caller moves. A regex with neither flag has a `lastIndex`
9
+ * that `exec`, `test`, `match`, `replace` and `split` never write. `pattern`
10
+ * is where that is checked: it refuses `g` and `y`, and hands back only
11
+ * those, with `source` and `flags` to compose it into another. A cursor — a
12
+ * sticky scan, a global replace — is made in the function that runs it, so
13
+ * each call owns its own.
14
+ */
15
+ declare const PATTERN: unique symbol;
16
+ /** A regex with no cursor: only `pattern` makes one. */
17
+ export type Pattern = Pick<RegExp, "exec" | "test" | "source" | "flags" | typeof Symbol.match | typeof Symbol.replace | typeof Symbol.split> & {
18
+ readonly [PATTERN]: true;
19
+ };
20
+ export declare function pattern(re: RegExp): Pattern;
21
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * pattern — `Pattern`: a regular expression held where every caller reaches
3
+ * it — module scope, a class static, a renderable's field — with no cursor
4
+ * for them to share.
5
+ *
6
+ * A `RegExp` carries `lastIndex`, and the `exec`, `test` and `replace` of a
7
+ * global or sticky one write it, so a global regex held at module scope is
8
+ * state that every caller moves. A regex with neither flag has a `lastIndex`
9
+ * that `exec`, `test`, `match`, `replace` and `split` never write. `pattern`
10
+ * is where that is checked: it refuses `g` and `y`, and hands back only
11
+ * those, with `source` and `flags` to compose it into another. A cursor — a
12
+ * sticky scan, a global replace — is made in the function that runs it, so
13
+ * each call owns its own.
14
+ */
15
+ export function pattern(re) {
16
+ // [LAW:no-silent-failure] A held cursor is refused where it is made, not found later as a match that began mid-string.
17
+ if (re.global || re.sticky) {
18
+ throw new TypeError(`/${re.source}/${re.flags} keeps a cursor in lastIndex; make it in the function that runs it`);
19
+ }
20
+ return re;
21
+ }
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { Segment } from "./segment.js";
21
21
  import { Measurement } from "./measure.js";
22
+ import { render } from "./protocol.js";
22
23
  // The share of the spare cells that goes on the left; the rest go on the right.
23
24
  // Right-alignment pads no right side at all, which is the reference's choice
24
25
  // and not a zero: a line that ends at the edge carries no trailing blanks.
@@ -39,7 +40,7 @@ export function placeBlock(renderable, align, options) {
39
40
  // (`Measurement.get` answers for it), and the block is then only as wide as
40
41
  // what it actually drew.
41
42
  const width = Measurement.get(options, renderable).maximum;
42
- const drawn = Segment.splitLines(renderable.render({ ...options, maxWidth: width, height: undefined }));
43
+ const drawn = Segment.splitLines(render(renderable, { ...options, maxWidth: width, height: undefined }));
43
44
  const [blockWidth, height] = Segment.getShape(drawn);
44
45
  const [left, right] = PADS[align](Math.max(0, options.maxWidth - blockWidth));
45
46
  return Segment.setShape(drawn, blockWidth, height).map((line) => [...pad(left), ...line, ...pad(right)]);
@@ -18,10 +18,12 @@
18
18
  */
19
19
  import { asCellCol, cellColToCodeUnitOffset, cellLen, graphemes, splitAtCells } from "./cells.js";
20
20
  import { divideLine } from "./wrap.js";
21
+ import { Memo } from "./memo.js";
21
22
  import { Segment } from "./segment.js";
22
23
  import { RichText, stripControlChars } from "./text.js";
23
24
  import { ReprHighlighter } from "./highlighter.js";
24
- import { drawable } from "./protocol.js";
25
+ import { drawable, render } from "./protocol.js";
26
+ import { pattern } from "./pattern.js";
25
27
  const reprHighlighter = new ReprHighlighter();
26
28
  /**
27
29
  * What a value is bounded by when whoever formats it has not seen it:
@@ -453,7 +455,7 @@ const elided = (dropped) => dropped > 0 ? [{ head: `... +${dropped}`, join: "",
453
455
  * An index is at most 15 digits: every such literal is a safe integer and
454
456
  * names itself, where `99999999999999999999` would name `1e20`'s key.
455
457
  */
456
- const BARE_KEY = /^(?![^]*\p{Default_Ignorable_Code_Point})(?:[\p{ID_Start}$_][\p{ID_Continue}$]*|0|[1-9]\d{0,14})$/u;
458
+ const BARE_KEY = pattern(/^(?![^]*\p{Default_Ignorable_Code_Point})(?:[\p{ID_Start}$_][\p{ID_Continue}$]*|0|[1-9]\d{0,14})$/u);
457
459
  /**
458
460
  * What Python's `str.isprintable` refuses \u2014 control, format, private-use,
459
461
  * unassigned, and every separator but the space \u2014 which Rich's repr escapes
@@ -461,9 +463,9 @@ const BARE_KEY = /^(?![^]*\p{Default_Ignorable_Code_Point})(?:[\p{ID_Start}$_][\
461
463
  * byte reaches the terminal as a control, and a format character hides or
462
464
  * reorders what is around it.
463
465
  */
464
- const UNPRINTABLE = /(?! )[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/gu;
466
+ const unprintable = () => /(?! )[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/gu;
465
467
  /** `value` in double quotes, every character that does not print escaped. */
466
- const quoted = (value) => JSON.stringify(value).replace(UNPRINTABLE, (c) => {
468
+ const quoted = (value) => JSON.stringify(value).replace(unprintable(), (c) => {
467
469
  const hex = c.codePointAt(0).toString(16).padStart(4, "0");
468
470
  return hex.length > 4 ? `\\u{${hex}}` : `\\u${hex}`;
469
471
  });
@@ -477,7 +479,7 @@ export class Pretty {
477
479
  indentGuides;
478
480
  highlighter;
479
481
  // [LAW:no-shared-mutable-globals] A private memo, written only by `_cutString`.
480
- cutStrings = new Map();
482
+ cutStrings = new Memo();
481
483
  constructor(data, options) {
482
484
  this.data = data;
483
485
  this.indent = options?.indent ?? 4;
@@ -491,7 +493,7 @@ export class Pretty {
491
493
  *render(options) {
492
494
  // `toText` has no `end`, for callers that set it inside a line; rendered,
493
495
  // a `Pretty` is a block of its own, as the reference's is.
494
- yield* this.toText(options).render(options);
496
+ yield* render(this.toText(options), options);
495
497
  yield Segment.line();
496
498
  }
497
499
  /** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
@@ -595,15 +597,12 @@ export class Pretty {
595
597
  * memoised per value.
596
598
  */
597
599
  _cutString(value, max) {
598
- let cut = this.cutStrings.get(value);
599
- if (cut === undefined) {
600
+ return this.cutStrings.get(value, () => {
600
601
  const clusters = graphemes(value);
601
- cut = clusters.length <= max
602
+ return clusters.length <= max
602
603
  ? quoted(value)
603
604
  : quoted(clusters.slice(0, max).join("")) + `+${clusters.length - max}`;
604
- this.cutStrings.set(value, cut);
605
- }
606
- return cut;
605
+ });
607
606
  }
608
607
  /**
609
608
  * A container, or the text standing in for one that is empty or past the
@@ -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 Hold, type StyleSyntaxError, type Theme } from "./style.js";
7
7
  import { ColorDepth } from "./color.js";
8
8
  import type { Highlighter } from "./highlighter.js";
9
9
  /**
@@ -17,6 +17,15 @@ import type { Highlighter } from "./highlighter.js";
17
17
  * wrapping and leaves the edge, so a line too wide is cut by the method here.
18
18
  */
19
19
  export type OverflowMethod = "fold" | "crop" | "ellipsis" | "ignore";
20
+ /**
21
+ * What draws frames — a `Live`, an `App` — as what draws in them sees it:
22
+ * the time a frame drawn now is drawn for, the host's clock read through its
23
+ * timeline for a view on it (`frameTime`, `./view.ts`). Not a `Clock`: under
24
+ * a paused timeline it holds, and under a reversed one it runs backwards.
25
+ */
26
+ export interface FrameHost {
27
+ readonly frameTime: number;
28
+ }
20
29
  export interface RenderOptions {
21
30
  /**
22
31
  * The cells this renderable may occupy. A count of cells, so a non-negative
@@ -68,8 +77,8 @@ export interface RenderOptions {
68
77
  highlighter?: Highlighter;
69
78
  /**
70
79
  * The depth the output will be encoded at, `null` when it carries no colour.
71
- * A renderable choosing between two ways of drawing something — the strip's
72
- * arrow or its divider — decides on the colours the terminal will draw at
80
+ * A renderable that measures what it draws — the strip's joiner, holding a
81
+ * seam's ink off its ground — measures the colours the terminal will draw at
73
82
  * this depth, which at 256 colours or fewer are not the colours it was
74
83
  * handed. Absent means truecolor.
75
84
  */
@@ -87,15 +96,56 @@ export interface RenderOptions {
87
96
  */
88
97
  onStyleError?: StyleErrorHandler;
89
98
  /**
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`).
99
+ * The reader asked for reduced motion — CSS's `prefers-reduced-motion`,
100
+ * which a terminal has no way to report, so a host is told. A renderable
101
+ * built for the time the host draws at draws every animation reduced, its
102
+ * motion taken out and its content kept on time (`shownAt`,
103
+ * `./view.ts`; `Animation.reduced`, `./animation.ts`). Absent,
104
+ * everything moves. Nothing here reads it from the environment.
105
+ */
106
+ reducedMotion?: boolean;
107
+ /**
108
+ * The frame being drawn: `time`, the moment it is drawn for, in seconds, on
109
+ * the clock its cells' `nextChange` stamps are read on, and `host`, the
110
+ * host drawing it, whose `frameTime` that moment was. It is how a
111
+ * renderable that lives across frames — a widget, a `Progress` — draws for
112
+ * the time without being rebuilt each frame with one, and without reading
113
+ * a clock. Absent, the frame is drawn for no moment — a `print` — and
114
+ * nothing drawn from it moves.
115
+ */
116
+ frame?: {
117
+ readonly time: number;
118
+ readonly host: FrameHost;
119
+ };
120
+ /**
121
+ * Told of every renderable `render` is asked for, as it is asked, so the
122
+ * calls arrive in document order: a container before what it holds, what
123
+ * it holds in the order it renders it. `"drawn"` is a renderable drawn at
124
+ * the width it was handed; `"held"` is one handed no column, which is still
125
+ * in the view but has nothing drawn for it (`render`). A renderable rendered
126
+ * twice is told twice. A render made to measure is not drawing and is not
127
+ * told (`measuring`).
96
128
  */
97
- onDraw?: (owner: object) => void;
129
+ onPresent?: (renderable: Renderable, presence: Presence) => void;
130
+ /**
131
+ * Told of a hold the frame is in that no cell of it may carry: an animation
132
+ * placed in the view reports its own, whether or not it drew a cell — one
133
+ * not yet entered draws none — and one a container crops away is still
134
+ * reported. It reaches whatever `frame` reaches, so a container that hands
135
+ * its child the frame's time hands back the child's hold with no word about
136
+ * either. [LAW:composability] Absent, nobody is listening. A measurement
137
+ * (`measuring`) and a held render (`holding`) tell too: what they find
138
+ * decides the frame's layout, so when it changes, the frame may.
139
+ */
140
+ onHold?: (hold: Hold) => void;
98
141
  }
142
+ /** The one field of the render options that says whether the reader wants motion. */
143
+ export type Motion = Pick<RenderOptions, "reducedMotion">;
144
+ /**
145
+ * Whether a renderable the view holds was drawn, or held: in the view, handed
146
+ * no column, and so not drawn (`render`).
147
+ */
148
+ export type Presence = "drawn" | "held";
99
149
  /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
100
150
  export declare const isAscii: (text: string) => boolean;
101
151
  /**
@@ -210,7 +260,10 @@ export declare function getStyle(options: RenderOptions, style: string | Style):
210
260
  * every bundled theme. So where the depth would turn a fixed colour of the
211
261
  * pair into a slot, the pair is dropped and the run is drawn on the
212
262
  * terminal's own foreground and background, which its theme does make
213
- * readable; its attributes still set it apart. A style whose colours are
263
+ * readable; its attributes still set it apart. Both are named, not left
264
+ * unset, so the colour of whatever the run sits in — a quote, a link — does
265
+ * not show through: Rich's code keeps its own colours inside a quote, and the
266
+ * terminal's are the ones it keeps here. A style whose colours are
214
267
  * slots already (`white on blue`), or that paints no ground (`bold magenta`),
215
268
  * was written for the terminal's own colours and is drawn as written. Rich
216
269
  * draws a highlighted line number the same way below 256 colours
@@ -238,6 +291,78 @@ export declare function groundedStyle(options: RenderOptions, style: Style): Sty
238
291
  * ignored the request and emitted its full natural width.
239
292
  */
240
293
  export declare function withCellWidth(options: RenderOptions): RenderOptions;
294
+ /** Under one column there is nothing to draw, or to measure. */
295
+ export declare function drawsNothing(options: {
296
+ readonly maxWidth: number;
297
+ }): boolean;
298
+ /**
299
+ * One render pass: the outermost `render` or `measure` call — a host's frame,
300
+ * a print — and every `next()` of what it returned, at any depth. Opaque: it
301
+ * is only an identity, which a `State` compares to say whether this pass made
302
+ * it.
303
+ */
304
+ export interface Pass {
305
+ readonly __pass: unique symbol;
306
+ }
307
+ /**
308
+ * `handler` run as if between passes: a callback a render makes to user code to
309
+ * report something it met, such as `onStyleError`, is an event handler, and may
310
+ * write what one may — print to a console, set a `State` — as React lets an
311
+ * event handler set state.
312
+ */
313
+ export declare function betweenPasses<T>(handler: () => T): T;
314
+ /** The pass a render or measure in progress belongs to, or `undefined` between passes. */
315
+ export declare function activePass(): Pass | undefined;
316
+ /**
317
+ * `effect` run once the render pass in progress has drawn its last segment,
318
+ * between passes, where it may write what an event handler may: React's commit
319
+ * phase. A render that learns something only drawing can tell it — the lines
320
+ * content shown through a `Viewport` runs to at the width it was handed —
321
+ * draws from what it learned and hands the commit here, rather than writing it
322
+ * as it draws.
323
+ *
324
+ * Effects run in the order they were handed over, which is document order. A
325
+ * measurement commits nothing, and neither does a pass that throws or whose
326
+ * caller stops iterating it early: nobody saw what it drew. Outside a pass — a
327
+ * renderable's `render` called directly — there is nothing to wait for, and
328
+ * `effect` runs now.
329
+ */
330
+ export declare function afterPass(effect: () => void): void;
331
+ /**
332
+ * `renderable` drawn under `options`: Rich's `Console.render`, which every
333
+ * renderable there is drawn through, so under one column nothing is.
334
+ *
335
+ * [LAW:single-enforcer] The one place that rule is kept. Every call of a
336
+ * `Renderable`'s `render` in `src/` goes through here — a container drawing
337
+ * its child, a renderable drawing the text it built, a host drawing a frame —
338
+ * and `test/seam/render-calls.test.ts` fails on one that does not. Before
339
+ * this, each container asked `drawsNothing` itself, and the ones that forgot
340
+ * drew a child handed no column anyway.
341
+ *
342
+ * It is where a render pass is entered (`Pass`), so it is the one place a
343
+ * write during render is refused at: a `State` set in a pass that did not make
344
+ * it throws (`./state.ts`).
345
+ *
346
+ * It is also the one reporter of what the view holds (`onPresent`). Under one
347
+ * column a renderable is held: in the view, with nothing of it on the frame.
348
+ * It is drawn all the same and what it drew dropped (`holding`), so what a held
349
+ * container is reported holding is what it draws, under the options it hands
350
+ * each child — one description of a container's traversal, not a second
351
+ * kept beside it.
352
+ */
353
+ export declare function render(renderable: Renderable, options: RenderOptions): Iterable<Segment>;
354
+ /**
355
+ * What `measurable` reports under `options`: asked in a render pass, as
356
+ * `render` draws in one, with `measuring`'s options.
357
+ *
358
+ * [LAW:single-enforcer] Every call of a `Measurable`'s `measure` in `src/`
359
+ * goes through here, as every `render` goes through `render`;
360
+ * `test/seam/render-calls.test.ts` holds both.
361
+ */
362
+ export declare function measure(measurable: Measurable, options: RenderOptions): {
363
+ minimum: number;
364
+ maximum: number;
365
+ };
241
366
  /**
242
367
  * The options a renderable should *lay out against*: `withCellWidth`, with an
243
368
  * unbounded offer resolved to the renderable's own natural width.
@@ -273,16 +398,6 @@ export declare function withCellWidth(options: RenderOptions): RenderOptions;
273
398
  * rather than zero, and saying so names the cause — where `String.repeat` and
274
399
  * `new Array` only ever name their own argument.
275
400
  */
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
401
  export declare function withBoundedWidth(options: RenderOptions, self: Measurable): RenderOptions;
287
402
  export interface Renderable {
288
403
  render(options: RenderOptions): Iterable<Segment>;