@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
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Easing and phase: the one vocabulary for "how far along" — every function
3
- * here returns progress in [0, 1].
2
+ * Easing and phase: the one vocabulary for "how far along" — a time in
3
+ * seconds becomes progress in [0, 1], and an ease reshapes that progress,
4
+ * usually but not always within [0, 1].
4
5
  *
5
6
  * Two kinds of curve, composed as `ease(phase(t))`:
6
7
  *
@@ -13,15 +14,26 @@
13
14
  * built-ins are CSS's easing functions (CSS Easing Functions Level 1):
14
15
  * `linear`, `cubic-bezier()` with its named curves, and `steps()` with its
15
16
  * jump terms, plus `sine` for pulses — a half-cosine, so a `pingPong`
16
- * through it brightens and dims with no corner at either end.
17
+ * through it brightens and dims with no corner at either end — and the
18
+ * physics-flavoured curves a cursor snapping, a counter settling or a
19
+ * panel sliding in want: `spring`, a damped harmonic oscillator released
20
+ * toward its rest, which is the one of them with parameters and so the one
21
+ * that also says how long it takes; `bounce`, `back-in`, `back-out` and
22
+ * `elastic`, Robert Penner's curves at the constants every animation
23
+ * library since has shipped. An ease that leaves [0, 1] — a spring or a
24
+ * back curve overshooting its rest — is still an ease: a `ColorRamp` holds
25
+ * its end colours where it does, and an effect's `Curve` saturates at its
26
+ * swing, so the overshoot is read by what can show it (a position, a
27
+ * count) and clipped by what cannot.
17
28
  *
18
29
  * [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. `t` is an
19
30
  * argument; whoever draws frames owns the clock and samples these at
20
31
  * whatever rate it likes, so the same `t` always gives the same progress.
21
32
  *
22
33
  * [LAW:one-type-per-behavior] An ease is a plain function value. CSS's named
23
- * curves are four instances of `cubicBezier`, not four implementations, and
24
- * `ColorRamp` and every effect take the same `Ease`.
34
+ * curves are four instances of `cubicBezier`, not four implementations, the
35
+ * spring in `EASES` is one `spring()`, and `ColorRamp` and every effect take
36
+ * the same `Ease`.
25
37
  *
26
38
  * Tier 0 of `src/core/`: imports nothing, so `themes/` and every later
27
39
  * `core/` module reach it downhill. [LAW:one-way-deps]
@@ -79,7 +91,7 @@ export function cubicBezier(x1, y1, x2, y2) {
79
91
  }
80
92
  return s;
81
93
  };
82
- return (x) => yAt(solve(Math.min(1, Math.max(0, x))));
94
+ return settled((p) => yAt(solve(p)));
83
95
  }
84
96
  /**
85
97
  * CSS `steps(n, position)`: progress held flat in `n` equal intervals, jumping
@@ -103,22 +115,183 @@ export function steps(n, position = "jump-end") {
103
115
  "jump-none": n - 1,
104
116
  }[position];
105
117
  const lift = jumpsAtStart ? 1 : 0;
106
- return (x) => {
107
- const p = Math.min(1, Math.max(0, x));
108
- return Math.min(jumps, Math.floor(p * n) + lift) / jumps;
109
- };
118
+ return settled((p) => Math.min(jumps, Math.floor(p * n) + lift) / jumps);
110
119
  }
120
+ /**
121
+ * The curve `shape` over [0, 1) as an ease that has arrived: its argument
122
+ * held to [0, 1], and exactly 1 at 1. Every built-in ease is one, so the two
123
+ * halves of the `Ease` contract — an argument in [0, 1], the end landed on —
124
+ * are enforced here and nowhere else. [LAW:single-enforcer] The end matters
125
+ * because a curve is 1 at its end only in the limit or up to a rounding — a
126
+ * spring settles to within `SPRING_REST`, the elastic's ringing never quite
127
+ * stops, a back curve's cubic or a bezier's Newton step lands an ulp off —
128
+ * and a count or a width eased by it must land on its target, not a
129
+ * thousandth or an ulp short.
130
+ */
131
+ export const settled = (shape) => (x) => {
132
+ const p = Math.min(1, Math.max(0, x));
133
+ return p === 1 ? 1 : shape(p);
134
+ };
111
135
  /** The identity ease: progress unchanged. */
112
- const linear = (x) => x;
136
+ const linear = settled((p) => p);
113
137
  /** CSS `step-end`: 0 until the very end. */
114
138
  const stepEnd = steps(1, "jump-end");
115
139
  /** A half-cosine, flat at both ends: the curve a breathing pulse wants. */
116
- const sine = (x) => (1 - Math.cos(Math.PI * x)) / 2;
140
+ const sine = settled((p) => (1 - Math.cos(Math.PI * p)) / 2);
141
+ /**
142
+ * How close to its rest a spring must come to count as settled: one part in a
143
+ * thousand of the way it moves, under a colour channel's step (1/255) and
144
+ * under any cell, so what rounds the spring's value to show it draws the rest
145
+ * from there on.
146
+ */
147
+ export const SPRING_REST = 0.001;
148
+ const DEFAULT_SPRING = { stiffness: 100, damping: 10, mass: 1 };
149
+ /**
150
+ * A damped harmonic oscillator, `m·x″ = −k·x − c·x′`, released at rest one
151
+ * unit from its equilibrium: the spring every animation library means. Its
152
+ * progress at `s` seconds is `1 − displacement(s)`, the closed-form solution
153
+ * of that equation in the regime its damping ratio `ζ = c / 2√(k·m)` puts it
154
+ * in — ringing past its rest for `ζ < 1`, creeping up to it for `ζ ≥ 1` — and
155
+ * its `seconds` is its settling time: the first moment after which the
156
+ * displacement stays within `SPRING_REST`, found once here by `crossing` on
157
+ * the bracket each regime knows its last crossing lies in, so a sample costs
158
+ * an exponential and a cosine whatever `x` is. [LAW:no-ambient-temporal-coupling]
159
+ *
160
+ * The ease is exactly 1 at `x = 1` (`settled`): settled means at rest. The
161
+ * `seconds` are the spring's to say, not the caller's: how long it rings is
162
+ * a fact about its physics, which is why this is the one ease that comes as
163
+ * a `Timing` — released at rest a unit from where it is going, at 1 when it
164
+ * has settled there.
165
+ */
166
+ export function spring(options = {}) {
167
+ const { stiffness, damping, mass } = { ...DEFAULT_SPRING, ...options };
168
+ for (const [name, v] of [["stiffness", stiffness], ["damping", damping], ["mass", mass]])
169
+ positive(`spring ${name}`, v);
170
+ const w0 = Math.sqrt(stiffness / mass);
171
+ const zeta = damping / (2 * Math.sqrt(stiffness * mass));
172
+ const decay = zeta * w0;
173
+ const { displacement, seconds } = zeta < 1 ? ringing(w0, zeta, decay) : creeping(w0, zeta, decay);
174
+ return { seconds, ease: settled((p) => 1 - displacement(p * seconds)) };
175
+ }
176
+ /**
177
+ * Where `|displacement|` falls through `SPRING_REST` between `lo` and `hi`,
178
+ * by bisection: the caller brackets the last such crossing, the displacement
179
+ * outside rest at `lo`, inside it at `hi` and falling in size between, and
180
+ * gets the settling time back to the bits 64 halvings leave.
181
+ * [LAW:single-enforcer] The one place the rest threshold is read against a
182
+ * motion.
183
+ */
184
+ function crossing(displacement, lo, hi) {
185
+ for (let i = 0; i < 64; i++) {
186
+ const mid = (lo + hi) / 2;
187
+ if (Math.abs(displacement(mid)) > SPRING_REST)
188
+ lo = mid;
189
+ else
190
+ hi = mid;
191
+ }
192
+ return hi;
193
+ }
194
+ /**
195
+ * Underdamped: `e^(−ζω₀s)·(cos ω_d s + (ζω₀/ω_d) sin ω_d s)` at the damped
196
+ * frequency `ω_d = ω₀√(1 − ζ²)`. Released at rest, its velocity is
197
+ * `−(ω₀²/ω_d)·e^(−ζω₀s)·sin ω_d s`, so its extrema sit at `s_k = kπ/ω_d`,
198
+ * where `|displacement| = e^(−ζω₀·s_k)`, each the same fraction of the last.
199
+ * The first extremum inside rest is the `K` with `e^(−ζω₀·s_K) ≤ SPRING_REST`;
200
+ * the spring never leaves rest after it, and before it the displacement
201
+ * falls in size from the extremum at `s_(K−1)` to its zero at
202
+ * `(Kπ − atan(ω_d/ζω₀)) / ω_d`, so the settling time is the crossing in that
203
+ * bracket. The decay envelope control theory reads settling time off would
204
+ * put it later — by up to half a swing, and without bound as `ζ → 1` — so
205
+ * the true crossing is found instead, and `seconds` is continuous in `ζ`.
206
+ */
207
+ function ringing(w0, zeta, decay) {
208
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
209
+ const ratio = decay / wd;
210
+ const displacement = (s) => Math.exp(-decay * s) * (Math.cos(wd * s) + ratio * Math.sin(wd * s));
211
+ const K = Math.ceil(Math.log(1 / SPRING_REST) / ((decay * Math.PI) / wd));
212
+ const extremum = ((K - 1) * Math.PI) / wd;
213
+ const zero = (K * Math.PI - Math.atan(1 / ratio)) / wd;
214
+ return { displacement, seconds: crossing(displacement, extremum, zero) };
215
+ }
216
+ /**
217
+ * Critically damped (`ζ = 1`): `e^(−ω₀s)·(1 + ω₀s)`. Overdamped (`ζ > 1`):
218
+ * two decaying exponentials at the rates `ζω₀ ∓ ω₀√(ζ² − 1)`, written as such
219
+ * rather than as `cosh + sinh` so neither grows before the other cancels it.
220
+ * Both only fall, so the settling time is the one crossing of `SPRING_REST`,
221
+ * bracketed between zero and a time the slower exponential alone puts it under.
222
+ */
223
+ function creeping(w0, zeta, decay) {
224
+ const { displacement, far } = zeta === 1 ? critical(w0) : overdamped(w0, zeta, decay);
225
+ return { displacement, seconds: crossing(displacement, 0, far) };
226
+ }
227
+ /** `(1 + u)·e^(−u)` in `u = ω₀s`, which is under 10⁻³ from u ≈ 9.24. */
228
+ function critical(w0) {
229
+ return { displacement: (s) => Math.exp(-w0 * s) * (1 + w0 * s), far: 10 / w0 };
230
+ }
231
+ /**
232
+ * The slower exponential alone bounds the sum, since the faster one's weight
233
+ * `1 − ratio` is negative. Its rate `ζω₀ − ω₀√(ζ² − 1)` is written as
234
+ * `ω₀ / (ζ + √(ζ² − 1))`, the same number without the subtraction, which for
235
+ * a large `ζ` cancels to zero and would make `far` infinite.
236
+ */
237
+ function overdamped(w0, zeta, decay) {
238
+ const root = Math.sqrt(zeta * zeta - 1);
239
+ const wh = w0 * root;
240
+ const slow = w0 / (zeta + root);
241
+ const fast = decay + wh;
242
+ const ratio = decay / wh;
243
+ return {
244
+ displacement: (s) => ((1 + ratio) * Math.exp(-slow * s) + (1 - ratio) * Math.exp(-fast * s)) / 2,
245
+ far: Math.log((1 + ratio) / (2 * SPRING_REST)) / slow,
246
+ };
247
+ }
248
+ /**
249
+ * Robert Penner's bounce (his `easeOutBounce`): a ball dropped onto its rest,
250
+ * four parabolic arcs each a quarter the height of the last, the constants
251
+ * his and every library's since. Each arc is the same parabola,
252
+ * `7.5625·(x − centre)² + floor`, set over its own span of `x`; the spans
253
+ * are in elevenths of 2.75, where the first arc meets the ground.
254
+ */
255
+ const BOUNCE_ARCS = [
256
+ { until: 1 / 2.75, centre: 0, floor: 0 },
257
+ { until: 2 / 2.75, centre: 1.5 / 2.75, floor: 0.75 },
258
+ { until: 2.5 / 2.75, centre: 2.25 / 2.75, floor: 0.9375 },
259
+ { until: Number.POSITIVE_INFINITY, centre: 2.625 / 2.75, floor: 0.984375 },
260
+ ];
261
+ const bounce = settled((p) => {
262
+ // The last arc's `until` is unbounded, so an arc is always found.
263
+ const arc = BOUNCE_ARCS.find(({ until }) => p < until);
264
+ return 7.5625 * (p - arc.centre) ** 2 + arc.floor;
265
+ });
266
+ /**
267
+ * Penner's back curves: a cubic that pulls back past its start before going
268
+ * (`back-in`), or overshoots its end before settling (`back-out`), by his
269
+ * overshoot constant 1.70158 — about a tenth of the way. His `(c + 1)·x³ −
270
+ * c·x²` is written as `x³ + c·x²·(x − 1)`, the same cubic with the overshoot
271
+ * term a product that is exactly zero at both ends, so the curve starts on 0
272
+ * and lands on 1 to the bit.
273
+ */
274
+ const BACK_OVERSHOOT = 1.70158;
275
+ const backIn = settled((p) => p ** 3 + BACK_OVERSHOOT * p ** 2 * (p - 1));
276
+ const backOut = settled((p) => {
277
+ const q = p - 1;
278
+ return 1 + q ** 3 + BACK_OVERSHOOT * q ** 2 * (q + 1);
279
+ });
280
+ /**
281
+ * Penner's elastic (his `easeOutElastic`): overshoots its end and rings back
282
+ * through it, with a period of 0.3 of the way under an amplitude of
283
+ * `2^(−10x)`, so each swing is `2^(−1.5)`, a little over a third, of the
284
+ * last — `2^(−10x)·sin((10x − 0.75)·2π/3) + 1`.
285
+ */
286
+ const ELASTIC_PERIOD = (2 * Math.PI) / 3;
287
+ const elastic = settled((p) => 2 ** (-10 * p) * Math.sin((10 * p - 0.75) * ELASTIC_PERIOD) + 1);
117
288
  /**
118
289
  * Every built-in ease by the name a template spells it with — CSS's keyword
119
290
  * names, plus `sine`, plus `step`: the spelling `ColorRamp` used before this
120
291
  * vocabulary existed, kept so every template written against it renders the
121
- * same. It is CSS's `step-end`, the same function value.
292
+ * same. It is CSS's `step-end`, the same function value. Then the
293
+ * physics-flavoured curves: `spring` is the default `spring()`, whose
294
+ * `seconds` are `DEFAULT_SPRING`'s; the rest are Penner's at his constants.
122
295
  */
123
296
  export const EASES = {
124
297
  linear,
@@ -130,6 +303,11 @@ export const EASES = {
130
303
  "step-end": stepEnd,
131
304
  step: stepEnd,
132
305
  sine,
306
+ spring: spring().ease,
307
+ bounce,
308
+ "back-in": backIn,
309
+ "back-out": backOut,
310
+ elastic,
133
311
  };
134
312
  /**
135
313
  * The gate a spelled ease crosses. [LAW:parse-dont-validate] — a name in, the
@@ -143,10 +321,33 @@ export function parseEase(name) {
143
321
  }
144
322
  return EASES[name];
145
323
  }
146
- function positiveSeconds(what, seconds) {
147
- if (!Number.isFinite(seconds) || seconds <= 0) {
148
- throw new RangeError(`${what} must be a positive number of seconds, got ${seconds}`);
149
- }
324
+ /** `v` is a positive, finite number, or this throws naming `what`; `unit` is the number's noun, as in `" of seconds"`. [LAW:single-enforcer] */
325
+ function positive(what, v, unit = "") {
326
+ if (!(Number.isFinite(v) && v > 0))
327
+ throw new RangeError(`${what} must be a positive number${unit}, got ${v}`);
328
+ }
329
+ /** Throws, naming `what`, unless `seconds` is a positive, finite number of seconds. */
330
+ export function positiveSeconds(what, seconds) {
331
+ positive(what, seconds, " of seconds");
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;
150
351
  }
151
352
  /**
152
353
  * The seconds into the current pass of a `period` at `t`: `t` taken modulo
@@ -158,9 +359,7 @@ function positiveSeconds(what, seconds) {
158
359
  * on the pass that begins there, and the answer is always under the period.
159
360
  */
160
361
  function wrapSeconds(what, t, period) {
161
- if (!Number.isFinite(t))
162
- throw new RangeError(`${what} needs a finite t, got ${t}`);
163
- const into = t % period;
362
+ const into = finiteTime(what, t) % period;
164
363
  if (into >= 0)
165
364
  return into;
166
365
  // A negative epsilon carried up rounds to the period itself; that is the
@@ -168,21 +367,108 @@ function wrapSeconds(what, t, period) {
168
367
  const carried = into + period;
169
368
  return carried >= period ? 0 : carried;
170
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;
171
386
  /**
172
387
  * The float after `x`: one ulp up, away from zero for a positive `x` and
173
- * 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.
174
391
  */
175
392
  function nextUp(x) {
176
393
  if (x === 0)
177
394
  return Number.MIN_VALUE;
178
- DOUBLE[0] = x;
179
- BITS[0] = BITS[0] + (x > 0 ? 1n : -1n);
180
- 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);
181
407
  }
182
- const DOUBLE = new Float64Array(1);
183
- const BITS = new BigUint64Array(DOUBLE.buffer);
184
- /** How many ulps the two roundings in `Beat.start`'s sum can put it under the hold's boundary; a walk past this is a hold too fine for the time. */
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. */
185
409
  const ULPS_OF_ROUNDING = 8;
410
+ /**
411
+ * The first float from `sum` up at which `reached` holds: a time built as a
412
+ * sum to land on a boundary — where a hold begins, where an animation's next
413
+ * part changes — that rounding put an ulp or two under it, walked up to the
414
+ * first moment on the far side. [LAW:single-enforcer] The one walk, so every
415
+ * boundary a time is carried onto is the moment the change shows, to the
416
+ * float. A walk longer than rounding can cost throws `finer()`'s message: the
417
+ * boundary is narrower than the floats around it are apart.
418
+ */
419
+ export function walkUp(sum, reached, finer) {
420
+ let x = sum;
421
+ for (let walked = 0; !reached(x); walked++) {
422
+ if (walked === ULPS_OF_ROUNDING)
423
+ throw new RangeError(finer());
424
+ x = nextUp(x);
425
+ }
426
+ return x;
427
+ }
428
+ /**
429
+ * The least float at which `reached` holds, `reached` being false up to some
430
+ * float and true from it on, found from `near`, a guess at it on either
431
+ * side. Where `walkUp` carries a sum onto a boundary on its own clock, and
432
+ * an ulp or two is all rounding can cost, this carries a moment across
433
+ * clocks (`./timeline.ts`): a host float is a step of `rate` on the
434
+ * animation's clock, so the host floats between the guess and the first one
435
+ * past the boundary are as many as the two clocks' resolutions are apart,
436
+ * which no cap bounds and is no error. The bracket is found by doubling the
437
+ * step out from `near`, and halved to two adjacent floats, so the answer is
438
+ * exact to the float and the cost is logarithmic in that distance.
439
+ */
440
+ export function firstReaching(near, reached) {
441
+ // [LAW:no-silent-failure] A step out from an infinite guess is NaN, and a
442
+ // bracket of NaN never closes.
443
+ if (!Number.isFinite(near))
444
+ throw new RangeError(`firstReaching needs a finite guess, got ${near}`);
445
+ let lo = near;
446
+ let hi = near;
447
+ // Out from `near` until the bracket is `lo` short of the boundary and
448
+ // `hi` past it, within the finite floats: reached at the least of them,
449
+ // it is reached always; not reached at the greatest, never.
450
+ for (let step = nextUp(Math.abs(near)) - Math.abs(near); reached(lo); step *= 2) {
451
+ if (lo === -Number.MAX_VALUE)
452
+ return Number.NEGATIVE_INFINITY;
453
+ hi = lo;
454
+ lo = Math.max(near - step, -Number.MAX_VALUE);
455
+ }
456
+ for (let step = nextUp(Math.abs(near)) - Math.abs(near); !reached(hi); step *= 2) {
457
+ if (hi === Number.MAX_VALUE)
458
+ return Number.POSITIVE_INFINITY;
459
+ lo = hi;
460
+ hi = Math.min(near + step, Number.MAX_VALUE);
461
+ }
462
+ for (;;) {
463
+ const mid = lo + (hi - lo) / 2;
464
+ if (mid <= lo || mid >= hi)
465
+ return hi;
466
+ if (reached(mid))
467
+ hi = mid;
468
+ else
469
+ lo = mid;
470
+ }
471
+ }
186
472
  /** `holds` as a `Beat`; a beat with no hold, or a hold that is not a positive number of seconds, throws. */
187
473
  export function beat(holds) {
188
474
  if (holds.length === 0)
@@ -217,40 +503,32 @@ export function beat(holds) {
217
503
  // are not exact in binary — a tenth of a second is not — and `at` would
218
504
  // then read it as the hold before, so it is walked up to the first float
219
505
  // `at` places in the hold. The walk is a function of the pass and the hold
220
- // alone, so every `t` in the hold still answers one float.
221
- const boundary = (what, pass, i, t) => {
222
- let began = pass * period + starts[i];
223
- for (let walked = 0; index(wrapSeconds(what, began, period)) !== i; walked++) {
224
- // The sum is off by the rounding of two operations, under the few
225
- // ulps allowed here; a walk that long means the hold is narrower
226
- // than the floats around `t` are apart, so no float starts it.
227
- if (walked === ULPS_OF_ROUNDING)
228
- throw new RangeError(`${what}: hold ${i} (${own[i]} s) is finer than time resolves near t = ${t}`);
229
- began = nextUp(began);
230
- }
231
- return began;
232
- };
233
- // Which pass `t` is in, as a whole number: `t - into` is a whole number of
234
- // periods but for rounding, which `round` takes off; carrying `starts[i]`
235
- // out of `t` itself would leave each sample its own rounding.
236
- 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}`);
237
514
  return {
238
515
  holds: own,
239
516
  period,
240
517
  at(t) {
241
518
  return index(wrapSeconds("Beat.at", t, period));
242
519
  },
243
- start(t) {
244
- const into = wrapSeconds("Beat.start", t, period);
245
- return boundary("Beat.start", pass(t, into), index(into), t);
520
+ lastChange(t) {
521
+ const { pass, into } = passAt("Beat.lastChange", t, period);
522
+ return boundary("Beat.lastChange", pass, index(into), t);
246
523
  },
247
524
  // The next hold's start, in the next pass when this hold is the last.
248
- end(t) {
249
- const into = wrapSeconds("Beat.end", t, period);
525
+ nextChange(t) {
526
+ const { pass, into } = passAt("Beat.nextChange", t, period);
250
527
  const i = index(into);
251
528
  const last = i === own.length - 1;
252
- return boundary("Beat.end", 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);
253
530
  },
531
+ reduced: () => STILL,
254
532
  };
255
533
  }
256
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,6 +8,7 @@
8
8
  */
9
9
  import { RichText } from "./text.js";
10
10
  import { JSONHighlighter } from "./highlighter.js";
11
+ import { measure, render } from "./protocol.js";
11
12
  const highlighter = new JSONHighlighter();
12
13
  export class JSONRenderable {
13
14
  text;
@@ -45,9 +46,9 @@ export class JSONRenderable {
45
46
  return new JSONRenderable(text);
46
47
  }
47
48
  *render(options) {
48
- yield* this.text.render(options);
49
+ yield* render(this.text, options);
49
50
  }
50
51
  measure(options) {
51
- return this.text.measure(options);
52
+ return measure(this.text, options);
52
53
  }
53
54
  }