@promptctl/rich-js 0.24.1 → 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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +21 -29
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +13 -16
  47. package/dist/core/strip.js +119 -110
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +132 -54
  73. package/dist/renderables/effect.js +188 -115
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  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 +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -16,9 +16,10 @@ import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
16
16
  // orchestrator reaching down for `Rule`. `RuleOptions` rides the same edge
17
17
  // rather than being restated here; see `rule()`.
18
18
  import { Rule } from "../renderables/rule.js";
19
- import { segmentsToString } from "./render.js";
19
+ import { segmentsToString, shownOn } from "./render.js";
20
20
  import { placeBlock } from "./place.js";
21
- import { isRenderable, render } from "./protocol.js";
21
+ import { activePass, isRenderable, measure, render } from "./protocol.js";
22
+ import { ListState, State, refuseRenderWrite } from "./state.js";
22
23
  // [LAW:dataflow-not-control-flow] With no live region claimed, stepping around
23
24
  // it is the text itself: every write takes the same path.
24
25
  const NO_LIVE_REGION = { around: (text) => text };
@@ -71,7 +72,7 @@ class TextRun {
71
72
  yield this.end;
72
73
  }
73
74
  measure(options) {
74
- return this.text.measure(options);
75
+ return measure(this.text, options);
75
76
  }
76
77
  }
77
78
  // A host with an environment but no streams — what a browser looks like from
@@ -177,7 +178,7 @@ export class Console {
177
178
  // compared with: the reference's `LogRender._last_time`, one per console.
178
179
  // [LAW:no-ambient-temporal-coupling] "same as the previous row" is a fact
179
180
  // about an earlier call, so it is state this console owns, not order luck.
180
- _lastLogTime = null;
181
+ _lastLogTime = new State(null);
181
182
  _style;
182
183
  _isTerminal;
183
184
  _interactive;
@@ -193,14 +194,15 @@ export class Console {
193
194
  _onStyleError;
194
195
  _asciiOnly;
195
196
  _highlighter;
196
- _recorded;
197
+ _recorded = new ListState();
197
198
  // [LAW:types-are-the-program] The capture state is carried in the *type*
198
199
  // of this field, not in a sentinel value. `null` = not capturing; any
199
200
  // string (including "") = capturing, this is the buffer. A paired
200
201
  // boolean+string would admit the impossible state "not capturing, buffer
201
202
  // non-empty" — `string | null` makes that unrepresentable.
202
- _capture;
203
- _liveRegion;
203
+ _capture = new State(null);
204
+ _liveRegion = new State(NO_LIVE_REGION);
205
+ _madeIn = activePass();
204
206
  constructor(options) {
205
207
  const environment = options?.environment ?? ambientEnvironment();
206
208
  const stream = boundStream(environment, options?.stderr ?? false);
@@ -209,14 +211,13 @@ export class Console {
209
211
  // [LAW:dataflow-not-control-flow] `isTTY` and `env` are forwarded
210
212
  // unconditionally, so detection stays inside the console's injected
211
213
  // environment rather than the ambient process's.
212
- const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env });
214
+ const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env, reducedMotion: options?.reducedMotion });
213
215
  // [LAW:single-enforcer] The console's one override, an explicit
214
216
  // `hyperlinks`, is applied here and nowhere else; every write encodes for
215
- // this value whole.
216
- this._destination = {
217
- colorSystem: resolved.colorSystem,
218
- hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
219
- };
217
+ // this value whole. [LAW:one-source-of-truth] The reader's motion
218
+ // preference lives here too, and `options` reads it back from here, so
219
+ // what is drawn and how it is encoded take one preference.
220
+ this._destination = { ...resolved, hyperlinks: options?.hyperlinks ?? resolved.hyperlinks };
220
221
  this._getSize = resolveGetSize(options, environment, stream);
221
222
  this._getDatetime = options?.getDatetime ?? (() => new Date());
222
223
  // [LAW:no-ambient-temporal-coupling] The theme is assigned before the
@@ -232,9 +233,6 @@ export class Console {
232
233
  this._highlighter = options?.highlighter ?? new ReprHighlighter();
233
234
  this._onStyleError = options?.onStyleError;
234
235
  this._asciiOnly = options?.asciiOnly ?? false;
235
- this._recorded = [];
236
- this._capture = null;
237
- this._liveRegion = NO_LIVE_REGION;
238
236
  }
239
237
  // --- Properties ---
240
238
  get size() {
@@ -256,8 +254,9 @@ export class Console {
256
254
  return this._destination.colorSystem;
257
255
  }
258
256
  /**
259
- * Where this console writes: the colour depth it draws at and whether it
260
- * emits OSC 8 hyperlinks, overrides applied — the value its output is
257
+ * Where this console writes: the colour depth it draws at, whether it
258
+ * emits OSC 8 hyperlinks, and whether its reader asked for reduced motion,
259
+ * overrides applied — the value its output is
261
260
  * encoded for, for anything that encodes on its behalf (`Live`).
262
261
  */
263
262
  get destination() {
@@ -287,6 +286,7 @@ export class Console {
287
286
  height: { rows: height, exact: false },
288
287
  isTerminal: this.isTerminal,
289
288
  asciiOnly: this._asciiOnly,
289
+ reducedMotion: this._destination.reducedMotion,
290
290
  theme: this._theme,
291
291
  onStyleError: this._onStyleError,
292
292
  colorSystem: this._destination.colorSystem,
@@ -474,7 +474,7 @@ export class Console {
474
474
  log(...args) {
475
475
  const options = this.options;
476
476
  const stamped = logTime(this._getDatetime());
477
- const repeated = stamped === this._lastLogTime;
477
+ const repeated = stamped === this._lastLogTime.get();
478
478
  const time = new RichText(stamped, { end: "" });
479
479
  time.stylize("log.time");
480
480
  const width = Math.min(time.cellLength, options.maxWidth - 1);
@@ -492,7 +492,7 @@ export class Console {
492
492
  this._writeSegments(emit({ rows: grid, closed: true, cropWidth: cropWidth + width }));
493
493
  // Remembered only once the row is written: a log that threw printed no
494
494
  // stamp, so the next row's must show.
495
- this._lastLogTime = stamped;
495
+ this._lastLogTime.set(stamped);
496
496
  }
497
497
  // [LAW:one-source-of-truth] `RuleOptions` is `Rule`'s, not a restatement of
498
498
  // it. `console.ts` used to declare its own copy and field-by-field forward
@@ -519,15 +519,15 @@ export class Console {
519
519
  * painted over by the first, so claiming one while another is held throws.
520
520
  */
521
521
  claimLiveRegion(region) {
522
- if (this._liveRegion !== NO_LIVE_REGION) {
522
+ if (this._liveRegion.get() !== NO_LIVE_REGION) {
523
523
  throw new Error("A live display is already running on this console; stop it before starting another.");
524
524
  }
525
- this._liveRegion = region;
525
+ this._liveRegion.set(region);
526
526
  // Letting go of a region no longer held — a second call, or after another
527
527
  // was claimed — leaves the one that is.
528
528
  return () => {
529
- if (this._liveRegion === region)
530
- this._liveRegion = NO_LIVE_REGION;
529
+ if (this._liveRegion.get() === region)
530
+ this._liveRegion.set(NO_LIVE_REGION);
531
531
  };
532
532
  }
533
533
  // --- Capture ---
@@ -537,60 +537,65 @@ export class Console {
537
537
  // the full text that would otherwise have been written. This is not a
538
538
  // tee — callers wanting both must compose explicitly.
539
539
  beginCapture() {
540
- this._capture = "";
540
+ this._capture.set("");
541
541
  }
542
542
  endCapture() {
543
- const result = this._capture ?? "";
544
- this._capture = null;
543
+ const result = this._capture.get() ?? "";
544
+ this._capture.set(null);
545
545
  return result;
546
546
  }
547
547
  // --- Export (when record:true) ---
548
548
  exportText({ clear = true } = {}) {
549
- const text = this._recorded.map((s) => s.text).join("");
549
+ const text = this._recorded.get().map((s) => s.text).join("");
550
550
  if (clear)
551
- this._recorded = [];
551
+ this._recorded.set([]);
552
552
  return text;
553
553
  }
554
554
  // [LAW:single-enforcer] `encodeHtml` draws the picture `exportLines`
555
555
  // resolves; nothing here interprets a `Style`.
556
556
  exportHtml({ theme, clear = true } = {}) {
557
- const html = encodeHtml(this._recorded, theme);
557
+ const html = encodeHtml(this._recorded.get(), theme);
558
558
  if (clear)
559
- this._recorded = [];
559
+ this._recorded.set([]);
560
560
  return html;
561
561
  }
562
562
  // [LAW:single-enforcer] As `exportHtml`: `encodeSvg` draws, this clears.
563
563
  exportSvg({ theme, title = "Rich", clear = true } = {}) {
564
- const svg = encodeSvg(this._recorded, { theme, title, width: this.width });
564
+ const svg = encodeSvg(this._recorded.get(), { theme, title, width: this.width });
565
565
  if (clear)
566
- this._recorded = [];
566
+ this._recorded.set([]);
567
567
  return svg;
568
568
  }
569
569
  // --- Internal ---
570
570
  // [LAW:single-enforcer] One encode call per write batch routes through the
571
571
  // same `segmentsToString` used by `renderToString`. Recording captures every
572
- // non-control segment for replay.
572
+ // non-control segment for replay, as its destination shows it.
573
573
  _writeSegments(segments) {
574
+ const shown = segments.map((segment) => shownOn(segment, this._destination));
574
575
  if (this._record) {
575
- for (const segment of segments) {
576
+ for (const segment of shown) {
576
577
  if (!segment.isControl)
577
578
  this._recorded.push(segment);
578
579
  }
579
580
  }
580
- const encoded = segmentsToString(segments, this._destination);
581
+ const encoded = segmentsToString(shown, this._destination);
581
582
  if (encoded.length > 0)
582
583
  this._write(encoded);
583
584
  }
584
585
  _write(text) {
586
+ // [LAW:single-enforcer] Every write is refused during a render this console was not made in, whatever
587
+ // the console records or captures: output is an effect, and one made mid-frame lands inside the frame.
588
+ refuseRenderWrite(this._madeIn);
585
589
  // [LAW:types-are-the-program] `_capture !== null` is a discriminator
586
590
  // narrow on `string | null`, not a sentinel comparison — the type
587
591
  // carries the active/inactive state. The early return is what makes
588
592
  // this a redirect (not a tee); see beginCapture for semantics.
589
- if (this._capture !== null) {
590
- this._capture += text;
593
+ const captured = this._capture.get();
594
+ if (captured !== null) {
595
+ this._capture.set(captured + text);
591
596
  return;
592
597
  }
593
598
  const target = this._file ?? defaultSink(this._stream);
594
- target.write(this._liveRegion.around(text));
599
+ target.write(this._liveRegion.get().around(text));
595
600
  }
596
601
  }
@@ -63,6 +63,18 @@ export type StepPosition = "jump-start" | "jump-end" | "jump-none" | "jump-both"
63
63
  * the very end.
64
64
  */
65
65
  export declare function steps(n: number, position?: StepPosition): Ease;
66
+ /**
67
+ * The curve `shape` over [0, 1) as an ease that has arrived: its argument
68
+ * held to [0, 1], and exactly 1 at 1. Every built-in ease is one, so the two
69
+ * halves of the `Ease` contract — an argument in [0, 1], the end landed on —
70
+ * are enforced here and nowhere else. [LAW:single-enforcer] The end matters
71
+ * because a curve is 1 at its end only in the limit or up to a rounding — a
72
+ * spring settles to within `SPRING_REST`, the elastic's ringing never quite
73
+ * stops, a back curve's cubic or a bezier's Newton step lands an ulp off —
74
+ * and a count or a width eased by it must land on its target, not a
75
+ * thousandth or an ulp short.
76
+ */
77
+ export declare const settled: (shape: (p: number) => number) => Ease;
66
78
  /**
67
79
  * A spring's physics: the mass on it, the stiffness of its coil and the
68
80
  * damping it moves against, in the units every animation library spells them
@@ -180,7 +192,25 @@ export interface Beat {
180
192
  * (`./animation.ts`).
181
193
  */
182
194
  nextChange(t: number): number;
195
+ /** The beat with its motion taken out: its first hold, held for good, as a loop holds its first value. */
196
+ reduced(): Beat;
183
197
  }
198
+ /**
199
+ * `t`, unless it is not a finite number of seconds, which throws naming
200
+ * `what`. [LAW:single-enforcer] The one refusal of a moment to draw at, for
201
+ * every loop and beat that reads time and every renderable drawn at a time.
202
+ */
203
+ export declare function finiteTime(what: string, t: number): number;
204
+ /**
205
+ * Which pass of a `period` `t` is in, as a whole number of either sign, and
206
+ * the seconds into it, read through the one wrap: what a `beat` and an
207
+ * `Animation.repeat` (`./animation.ts`) both ask, so the two agree on the
208
+ * pass of every float, a boundary included.
209
+ */
210
+ export declare function passAt(what: string, t: number, period: number): {
211
+ readonly pass: number;
212
+ readonly into: number;
213
+ };
184
214
  /**
185
215
  * The first float from `sum` up at which `reached` holds: a time built as a
186
216
  * sum to land on a boundary — where a hold begins, where an animation's next
@@ -128,7 +128,7 @@ export function steps(n, position = "jump-end") {
128
128
  * and a count or a width eased by it must land on its target, not a
129
129
  * thousandth or an ulp short.
130
130
  */
131
- const settled = (shape) => (x) => {
131
+ export const settled = (shape) => (x) => {
132
132
  const p = Math.min(1, Math.max(0, x));
133
133
  return p === 1 ? 1 : shape(p);
134
134
  };
@@ -330,6 +330,25 @@ function positive(what, v, unit = "") {
330
330
  export function positiveSeconds(what, seconds) {
331
331
  positive(what, seconds, " of seconds");
332
332
  }
333
+ /** A beat of one hold that never ends: hold 0 at every time, never changing — what every beat is reduced to. */
334
+ const STILL = {
335
+ holds: [Number.POSITIVE_INFINITY],
336
+ period: Number.POSITIVE_INFINITY,
337
+ at: () => 0,
338
+ lastChange: () => Number.NEGATIVE_INFINITY,
339
+ nextChange: () => Number.POSITIVE_INFINITY,
340
+ reduced: () => STILL,
341
+ };
342
+ /**
343
+ * `t`, unless it is not a finite number of seconds, which throws naming
344
+ * `what`. [LAW:single-enforcer] The one refusal of a moment to draw at, for
345
+ * every loop and beat that reads time and every renderable drawn at a time.
346
+ */
347
+ export function finiteTime(what, t) {
348
+ if (!Number.isFinite(t))
349
+ throw new RangeError(`${what} needs a finite t, got ${t}`);
350
+ return t;
351
+ }
333
352
  /**
334
353
  * The seconds into the current pass of a `period` at `t`: `t` taken modulo
335
354
  * the period, into [0, period), before zero too. [LAW:single-enforcer] The
@@ -340,9 +359,7 @@ export function positiveSeconds(what, seconds) {
340
359
  * on the pass that begins there, and the answer is always under the period.
341
360
  */
342
361
  function wrapSeconds(what, t, period) {
343
- if (!Number.isFinite(t))
344
- throw new RangeError(`${what} needs a finite t, got ${t}`);
345
- const into = t % period;
362
+ const into = finiteTime(what, t) % period;
346
363
  if (into >= 0)
347
364
  return into;
348
365
  // A negative epsilon carried up rounds to the period itself; that is the
@@ -350,19 +367,44 @@ function wrapSeconds(what, t, period) {
350
367
  const carried = into + period;
351
368
  return carried >= period ? 0 : carried;
352
369
  }
370
+ /**
371
+ * Which pass of a `period` `t` is in, as a whole number of either sign, and
372
+ * the seconds into it, read through the one wrap: what a `beat` and an
373
+ * `Animation.repeat` (`./animation.ts`) both ask, so the two agree on the
374
+ * pass of every float, a boundary included.
375
+ */
376
+ export function passAt(what, t, period) {
377
+ const into = wrapSeconds(what, t, period);
378
+ // `t - into` is a whole number of periods but for rounding, which `round`
379
+ // takes off; carrying a pass's start out of `t` itself would leave each
380
+ // sample its own rounding. `+ 0` turns the `-0` a small negative `t` rounds
381
+ // to into pass 0, so the pass is a plain integer whoever reads it.
382
+ return { pass: Math.round((t - into) / period) + 0, into };
383
+ }
384
+ /** The least normal double, 2^-1022: below it every double is a multiple of `Number.MIN_VALUE`. */
385
+ const MIN_NORMAL = 2 ** -1022;
353
386
  /**
354
387
  * The float after `x`: one ulp up, away from zero for a positive `x` and
355
- * toward it for a negative one, read through the bits of the double.
388
+ * toward it for a negative one. Computed, not read through the bits of the
389
+ * double: a buffer per call costs an allocation on every boundary walked, and
390
+ * one at module scope is state every caller shares.
356
391
  */
357
392
  function nextUp(x) {
358
393
  if (x === 0)
359
394
  return Number.MIN_VALUE;
360
- DOUBLE[0] = x;
361
- BITS[0] = BITS[0] + (x > 0 ? 1n : -1n);
362
- return DOUBLE[0];
395
+ const a = Math.abs(x);
396
+ if (a < MIN_NORMAL)
397
+ return x + Number.MIN_VALUE;
398
+ // `log2` may round across a power of two; the two checks put `a` in [2^e, 2^(e+1)).
399
+ let e = Math.floor(Math.log2(a));
400
+ if (2 ** e > a)
401
+ e -= 1;
402
+ else if (2 ** (e + 1) <= a)
403
+ e += 1;
404
+ const ulp = 2 ** (e - 52);
405
+ // Toward zero from a power of two, the floats below it are half as far apart.
406
+ return x > 0 ? x + ulp : x + (a === 2 ** e && a > MIN_NORMAL ? ulp / 2 : ulp);
363
407
  }
364
- const DOUBLE = new Float64Array(1);
365
- const BITS = new BigUint64Array(DOUBLE.buffer);
366
408
  /** How many ulps the roundings in a sum naming a boundary can put it under that boundary; a walk past this is a boundary finer than time resolves there. */
367
409
  const ULPS_OF_ROUNDING = 8;
368
410
  /**
@@ -461,12 +503,14 @@ export function beat(holds) {
461
503
  // are not exact in binary — a tenth of a second is not — and `at` would
462
504
  // then read it as the hold before, so it is walked up to the first float
463
505
  // `at` places in the hold. The walk is a function of the pass and the hold
464
- // alone, so every `t` in the hold still answers one float.
465
- const boundary = (what, pass, i, t) => walkUp(pass * period + starts[i], (began) => index(wrapSeconds(what, began, period)) === i, () => `${what}: hold ${i} (${own[i]} s) is finer than time resolves near t = ${t}`);
466
- // Which pass `t` is in, as a whole number: `t - into` is a whole number of
467
- // periods but for rounding, which `round` takes off; carrying `starts[i]`
468
- // out of `t` itself would leave each sample its own rounding.
469
- const pass = (t, into) => Math.round((t - into) / period);
506
+ // alone, so every `t` in the hold still answers one float. It is walked
507
+ // until `at` places it in this hold of this pass: with one hold, every
508
+ // float is in hold 0, and only the pass tells the boundary from the float
509
+ // under it.
510
+ const boundary = (what, p, i, t) => walkUp(p * period + starts[i], (began) => {
511
+ const { pass, into } = passAt(what, began, period);
512
+ return index(into) === i && pass === p;
513
+ }, () => `${what}: hold ${i} (${own[i]} s) is finer than time resolves near t = ${t}`);
470
514
  return {
471
515
  holds: own,
472
516
  period,
@@ -474,16 +518,17 @@ export function beat(holds) {
474
518
  return index(wrapSeconds("Beat.at", t, period));
475
519
  },
476
520
  lastChange(t) {
477
- const into = wrapSeconds("Beat.lastChange", t, period);
478
- return boundary("Beat.lastChange", pass(t, into), index(into), t);
521
+ const { pass, into } = passAt("Beat.lastChange", t, period);
522
+ return boundary("Beat.lastChange", pass, index(into), t);
479
523
  },
480
524
  // The next hold's start, in the next pass when this hold is the last.
481
525
  nextChange(t) {
482
- const into = wrapSeconds("Beat.nextChange", t, period);
526
+ const { pass, into } = passAt("Beat.nextChange", t, period);
483
527
  const i = index(into);
484
528
  const last = i === own.length - 1;
485
- return boundary("Beat.nextChange", pass(t, into) + (last ? 1 : 0), last ? 0 : i + 1, t);
529
+ return boundary("Beat.nextChange", pass + (last ? 1 : 0), last ? 0 : i + 1, t);
486
530
  },
531
+ reduced: () => STILL,
487
532
  };
488
533
  }
489
534
  /** The three phase shapes. Each takes seconds and returns a `Phase`. */
@@ -4,7 +4,7 @@
4
4
  import { Segment } from "./segment.js";
5
5
  import type { Style } from "./style.js";
6
6
  import type { Renderable, RenderOptions } from "./protocol.js";
7
- export declare const EMOJI: Record<string, string>;
7
+ export declare const EMOJI: Readonly<Record<string, string>>;
8
8
  /**
9
9
  * Replaces :shortcode: patterns in text with emoji characters.
10
10
  */
@@ -324,12 +324,11 @@ export const EMOJI = {
324
324
  radioactive: "☢",
325
325
  biohazard: "☣",
326
326
  };
327
- const EMOJI_PATTERN = /:([a-z0-9_]+(?:-(?:emoji|text))?):/g;
328
327
  /**
329
328
  * Replaces :shortcode: patterns in text with emoji characters.
330
329
  */
331
330
  export function emojiReplace(text, defaultVariant) {
332
- return text.replace(EMOJI_PATTERN, (_match, code) => {
331
+ return text.replace(/:([a-z0-9_]+(?:-(?:emoji|text))?):/g, (_match, code) => {
333
332
  let variant = defaultVariant;
334
333
  let name = code;
335
334
  // Check for inline variant suffix
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { cellLen, graphemes } from "./cells.js";
16
16
  import { escapeAttribute, escapeText, exportCanvas, exportLines, } from "./export-lines.js";
17
+ import { pattern } from "./pattern.js";
17
18
  const BLINK_KEYFRAMES = "rich-blink";
18
19
  // [LAW:dataflow-not-control-flow] Each enum-valued field of a look is a table
19
20
  // from its values to the declarations it adds, so every value is spelled once
@@ -68,7 +69,7 @@ function glyphCss(look) {
68
69
  const ANCHOR_CSS = "all:unset;cursor:revert;outline:revert";
69
70
  const span = (css, content) => `<span style="${escapeAttribute(css.join(";"))}">${content}</span>`;
70
71
  /** Text of printable ASCII only: one cell a character, nothing to box. */
71
- const ONE_CELL = /^[\x20-\x7e]*$/;
72
+ const ONE_CELL = pattern(/^[\x20-\x7e]*$/);
72
73
  /**
73
74
  * A box exactly `cells` of the monospace font wide (`ch` is one of its cells)
74
75
  * for glyphs the browser would otherwise draw at a fallback font's width,
@@ -62,15 +62,15 @@ const ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
62
62
  * surrogates. One of them leaves a whole SVG unparsable, not one glyph missing.
63
63
  * Each takes no cell, so dropping it moves nothing on the grid.
64
64
  */
65
- const NOT_XML = /[\x00-\x08\x0B\x0C\x0E-\x1F￾￿]|\p{Cs}/gu;
65
+ const withoutNotXml = (text) => text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F￾￿]|\p{Cs}/gu, "");
66
66
  /**
67
67
  * Text as character data in HTML or SVG markup. Both exporters write markup,
68
68
  * and [LAW:single-enforcer] one escaper is what keeps a title or a run that is
69
69
  * safe in one format from being live markup, or no markup at all, in the other.
70
70
  */
71
- export const escapeText = (text) => text.replace(NOT_XML, "").replace(/[&<>]/g, (c) => ENTITIES[c]);
71
+ export const escapeText = (text) => withoutNotXml(text).replace(/[&<>]/g, (c) => ENTITIES[c]);
72
72
  /** A value inside a double-quoted attribute in HTML or SVG markup. */
73
- export const escapeAttribute = (value) => value.replace(NOT_XML, "").replace(/[&"<>]/g, (c) => ENTITIES[c]);
73
+ export const escapeAttribute = (value) => withoutNotXml(value).replace(/[&"<>]/g, (c) => ENTITIES[c]);
74
74
  /**
75
75
  * How far a dim glyph moves toward its background — Rich's own factor, so an
76
76
  * export of the same program matches the library this ports.
@@ -10,24 +10,25 @@ export declare class NullHighlighter extends Highlighter {
10
10
  highlight(_text: RichText): void;
11
11
  }
12
12
  export declare class RegexHighlighter extends Highlighter {
13
- static highlights: (string | RegExp)[];
13
+ /** Each pattern's `source` and `flags`: a regex is compiled from them per call, so a held one keeps no cursor. */
14
+ static readonly highlights: readonly (string | Pick<RegExp, "source" | "flags">)[];
14
15
  /** Style applied to matches. When ending with "." it acts as a namespace
15
16
  * prefix: named groups become `baseStyle + groupName` (e.g. "repr." + "url"
16
17
  * → "repr.url"). Otherwise it's used as a literal style for all matches
17
18
  * (e.g. "bold red" applies "bold red" to every named group). */
18
- static baseStyle: string;
19
+ static readonly baseStyle: string;
19
20
  highlight(text: RichText): void;
20
21
  }
21
22
  export declare class ReprHighlighter extends RegexHighlighter {
22
- static baseStyle: string;
23
- static highlights: RegExp[];
23
+ static readonly baseStyle = "repr.";
24
+ static readonly highlights: typeof RegexHighlighter.highlights;
24
25
  }
25
26
  export declare class JSONHighlighter extends RegexHighlighter {
26
- static baseStyle: string;
27
- static highlights: RegExp[];
27
+ static readonly baseStyle = "json.";
28
+ static readonly highlights: typeof RegexHighlighter.highlights;
28
29
  highlight(text: RichText): void;
29
30
  }
30
31
  export declare class ISO8601Highlighter extends RegexHighlighter {
31
- static baseStyle: string;
32
- static highlights: string[];
32
+ static readonly baseStyle = "iso8601.";
33
+ static readonly highlights: typeof RegexHighlighter.highlights;
33
34
  }
@@ -18,6 +18,7 @@ export class NullHighlighter extends Highlighter {
18
18
  }
19
19
  // --- RegexHighlighter ---
20
20
  export class RegexHighlighter extends Highlighter {
21
+ /** Each pattern's `source` and `flags`: a regex is compiled from them per call, so a held one keeps no cursor. */
21
22
  static highlights = [];
22
23
  /** Style applied to matches. When ending with "." it acts as a namespace
23
24
  * prefix: named groups become `baseStyle + groupName` (e.g. "repr." + "url"
@@ -34,8 +35,8 @@ export class RegexHighlighter extends Highlighter {
34
35
  // `d` reports where each group matched, as Python's `match.span(name)`
35
36
  // does; searching the match for the group's text finds the first copy
36
37
  // of it, which is not always the one the group captured.
37
- const source = pattern instanceof RegExp ? pattern.source : pattern;
38
- const flags = new Set([...(pattern instanceof RegExp ? pattern.flags : ""), "g", "d"]);
38
+ const source = typeof pattern === "string" ? pattern : pattern.source;
39
+ const flags = new Set([...(typeof pattern === "string" ? "" : pattern.flags), "g", "d"]);
39
40
  for (const match of text.plain.matchAll(new RegExp(source, [...flags].join("")))) {
40
41
  if (match.indices === undefined)
41
42
  throw new Error("a match of a `d`-flag pattern carries indices");
package/dist/core/json.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { RichText } from "./text.js";
10
10
  import { JSONHighlighter } from "./highlighter.js";
11
- import { render } from "./protocol.js";
11
+ import { measure, render } from "./protocol.js";
12
12
  const highlighter = new JSONHighlighter();
13
13
  export class JSONRenderable {
14
14
  text;
@@ -49,6 +49,6 @@ export class JSONRenderable {
49
49
  yield* render(this.text, options);
50
50
  }
51
51
  measure(options) {
52
- return this.text.measure(options);
52
+ return measure(this.text, options);
53
53
  }
54
54
  }
@@ -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 { drawsNothing, isMeasurable, measuring } from "./protocol.js";
4
+ import { drawsNothing, isMeasurable, measure } from "./protocol.js";
5
5
  export class Measurement {
6
6
  minimum;
7
7
  maximum;
@@ -49,7 +49,7 @@ export class Measurement {
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
  }