@promptctl/rich-js 0.27.0 → 0.28.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 (110) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +36 -17
  4. package/dist/core/animation.js +61 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +93 -5
  17. package/dist/core/color.d.ts +6 -7
  18. package/dist/core/color.js +11 -13
  19. package/dist/core/console.d.ts +18 -1
  20. package/dist/core/console.js +24 -5
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/env.d.ts +6 -0
  24. package/dist/core/env.js +9 -1
  25. package/dist/core/export-html.js +4 -3
  26. package/dist/core/export-lines.d.ts +2 -0
  27. package/dist/core/export-lines.js +7 -3
  28. package/dist/core/export-svg.js +6 -5
  29. package/dist/core/json.js +9 -2
  30. package/dist/core/markup.js +4 -3
  31. package/dist/core/orient.d.ts +68 -0
  32. package/dist/core/orient.js +224 -0
  33. package/dist/core/osc8.d.ts +12 -5
  34. package/dist/core/osc8.js +14 -7
  35. package/dist/core/pretty.js +22 -18
  36. package/dist/core/protocol.d.ts +27 -6
  37. package/dist/core/protocol.js +14 -6
  38. package/dist/core/render.d.ts +8 -3
  39. package/dist/core/render.js +18 -5
  40. package/dist/core/seal.d.ts +26 -0
  41. package/dist/core/seal.js +75 -0
  42. package/dist/core/segment.d.ts +9 -1
  43. package/dist/core/segment.js +11 -1
  44. package/dist/core/style.d.ts +29 -1
  45. package/dist/core/style.js +41 -5
  46. package/dist/core/text-order.d.ts +51 -0
  47. package/dist/core/text-order.js +50 -0
  48. package/dist/core/text.d.ts +10 -0
  49. package/dist/core/text.js +133 -87
  50. package/dist/core/timeline.js +6 -6
  51. package/dist/core/view.d.ts +1 -1
  52. package/dist/core/view.js +5 -4
  53. package/dist/core/wrap.d.ts +24 -6
  54. package/dist/core/wrap.js +82 -16
  55. package/dist/host/app.d.ts +44 -3
  56. package/dist/host/app.js +111 -6
  57. package/dist/host/index.d.ts +3 -2
  58. package/dist/host/index.js +1 -0
  59. package/dist/host/input.d.ts +35 -2
  60. package/dist/host/input.js +88 -4
  61. package/dist/host/terminal-colors.d.ts +18 -0
  62. package/dist/host/terminal-colors.js +46 -0
  63. package/dist/host/terminal-host.d.ts +2 -1
  64. package/dist/index.d.ts +5 -4
  65. package/dist/index.js +2 -2
  66. package/dist/renderables/change.d.ts +1 -3
  67. package/dist/renderables/change.js +4 -6
  68. package/dist/renderables/effect.d.ts +60 -59
  69. package/dist/renderables/effect.js +91 -81
  70. package/dist/renderables/effects.d.ts +37 -8
  71. package/dist/renderables/effects.js +185 -52
  72. package/dist/renderables/frames.d.ts +1 -10
  73. package/dist/renderables/frames.js +1 -16
  74. package/dist/renderables/glyph-effect.d.ts +12 -13
  75. package/dist/renderables/glyph-effect.js +34 -32
  76. package/dist/renderables/glyph-effects.d.ts +27 -8
  77. package/dist/renderables/glyph-effects.js +75 -7
  78. package/dist/renderables/lifecycle.d.ts +5 -7
  79. package/dist/renderables/lifecycle.js +6 -7
  80. package/dist/renderables/markdown.js +4 -2
  81. package/dist/renderables/panel.js +6 -4
  82. package/dist/renderables/progressBar.d.ts +3 -3
  83. package/dist/renderables/progressBar.js +16 -13
  84. package/dist/renderables/syntax.js +7 -2
  85. package/dist/renderables/table.js +2 -1
  86. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  87. package/dist/template-bindings/animation-funcs.js +115 -32
  88. package/dist/template-bindings/index.d.ts +7 -5
  89. package/dist/template-bindings/index.js +8 -5
  90. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  91. package/dist/template-bindings/palette-funcs.js +1 -1
  92. package/dist/themes/buildPalette.d.ts +3 -2
  93. package/dist/themes/buildPalette.js +2 -2
  94. package/dist/themes/data/index.js +1 -4
  95. package/dist/themes/data/textual-ansi.js +0 -143
  96. package/dist/themes/ramp.d.ts +1 -1
  97. package/dist/themes/ramp.js +1 -1
  98. package/dist/themes/registry.d.ts +6 -8
  99. package/dist/themes/registry.js +2 -4
  100. package/dist/themes/terminalThemes.d.ts +4 -11
  101. package/dist/themes/terminalThemes.js +31 -47
  102. package/dist/widgets/button.d.ts +1 -1
  103. package/dist/widgets/button.js +6 -6
  104. package/dist/widgets/checkbox.js +4 -4
  105. package/dist/widgets/dropdown.d.ts +1 -1
  106. package/dist/widgets/dropdown.js +7 -5
  107. package/dist/widgets/themed-widget.js +3 -3
  108. package/dist/widgets/toggle.d.ts +1 -1
  109. package/dist/widgets/toggle.js +5 -5
  110. package/package.json +5 -4
package/README.md CHANGED
@@ -397,6 +397,7 @@ npm run demo
397
397
  | `npm run animation-playground` | animation-playground | Every kind of motion — tweens, moving ramps, a marching border, glyph and attribute effects, transitions, a list that comes and goes, a marquee, staggers and a blinking caret — on one timeline, with buttons to pause, reverse, scrub, change speed, reduce motion, drop to sixteen colours or none, and paint at one frame a second or sixty. |
398
398
  | `npm run markup-plugins` | rich-markup-plugins | Custom markup tags registered through `MarkupRegistry`. Prints once and exits. |
399
399
  | `npm run viewport` | rich-viewport | A selection you move through a list taller than its window with the arrow keys, `j`/`k`, PgUp/PgDn and Home/End, kept in view by `Viewport.ensureVisible`; the mouse wheel scrolls the window, and `q` quits. |
400
+ | `npm run terminal-colors` | terminal-colors | Your terminal asked for its own foreground and background with OSC 10 and OSC 11, shown as swatches, and whether it answered or the colours are assumed. |
400
401
  | `npm run terminal-host` | terminal-host | Each key you press printed as the bytes the terminal sends for it, and each resize as the new size — a program written against `TerminalHost` alone. `examples/terminal-host/embed.ts` runs the same program on an xterm.js terminal in your own page. |
401
402
 
402
403
  `Ctrl-C` quits any demo that stays running.
@@ -107,3 +107,45 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
107
107
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
108
108
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
109
109
  SOFTWARE.
110
+
111
+ --------------------------------------------------------------------------------
112
+
113
+ src/core/bidi-data.ts holds data derived from the Unicode Character Database.
114
+
115
+ UNICODE LICENSE V3
116
+
117
+ COPYRIGHT AND PERMISSION NOTICE
118
+
119
+ Copyright (c) 1991-2026 Unicode, Inc.
120
+
121
+ NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING,
122
+ INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR SOFTWARE, YOU
123
+ UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS
124
+ OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY,
125
+ DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
126
+
127
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
128
+ data files and any associated documentation (the "Data Files") or software and
129
+ any associated documentation (the "Software") to deal in the Data Files or
130
+ Software without restriction, including without limitation the rights to use,
131
+ copy, modify, merge, publish, distribute, and/or sell copies of the Data Files
132
+ or Software, and to permit persons to whom the Data Files or Software are
133
+ furnished to do so, provided that either (a) this copyright and permission
134
+ notice appear with all copies of the Data Files or Software, or (b) this
135
+ copyright and permission notice appear in associated Documentation.
136
+
137
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
138
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
139
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD
140
+ PARTY RIGHTS.
141
+
142
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE
143
+ LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY
144
+ DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
145
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
146
+ CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
147
+
148
+ Except as contained in this notice, the name of a copyright holder shall not be
149
+ used in advertising or otherwise to promote the sale, use or other dealings in
150
+ these Data Files or Software without prior written authorization of the
151
+ copyright holder.
@@ -9,7 +9,8 @@
9
9
  * changes every frame, however close together they are drawn. An early
10
10
  * answer costs a host a frame that draws what the last one did; a late one
11
11
  * skips a change. So an animation that cannot say answers `t`, as
12
- * `continuous` does. A value drawn as cells carries this answer on every
12
+ * `continuous` does. An answer that depends on what the frame drew, an
13
+ * effect's, is told by that drawing (`Moves`). A value drawn as cells carries this answer on every
13
14
  * cell (`Style.hold`, both ends of the hold below), so a host holding
14
15
  * the composed frame reads it there (`Segment.nextChange`); a value that is
15
16
  * not drawn — a number, a colour — has `nextChange` as its only way to say
@@ -76,14 +77,31 @@ import { type Beat } from "./easing.js";
76
77
  import { type Measurable, type Motion, type Renderable, type RenderOptions } from "./protocol.js";
77
78
  import { Segment } from "./segment.js";
78
79
  import { Style, type Hold } from "./style.js";
80
+ /**
81
+ * What a frame's drawing said of the hold the element named `key`, drawn at
82
+ * `t`, is in, or nothing if the frame heard of no such drawing: handed to an
83
+ * animation asked about a frame already drawn, so an answer only a drawing
84
+ * knows goes up through the combinators that remapped the time. It reads by
85
+ * `key` and not by the effect, which `reduced()` and a `repeat`'s `nth` build
86
+ * anew at each asking, and by `t`, since one element may be drawn at two
87
+ * moments. Parts sharing both share the intersection of what was drawn:
88
+ * narrower and never wider. A drawing a container hid, by not handing on its
89
+ * options, reads as none and stays early, unless a part sharing its key and
90
+ * `t` told. A measurement, which draws nothing, answers held for good for
91
+ * every part (`UNLAID`). An animation that does not read it ignores it, and
92
+ * one that forwards it to no part stays early. [LAW:one-source-of-truth] The
93
+ * frame's one drawing says it, so the answer cannot disagree with what the
94
+ * frame shows.
95
+ */
96
+ export type Moves = (t: number, key: string) => Hold | undefined;
79
97
  /** A value of `T` at a time in seconds, and the hold it is in: what every animation answers, whatever its values. */
80
98
  export interface Timed<T> {
81
99
  /** The value at `t` seconds. */
82
100
  at(t: number): T;
83
101
  /** The first moment, never before `t`, at which the value may stop being `at(t)`: `t` while it moves, `Infinity` once it never will. */
84
- nextChange(t: number): number;
102
+ nextChange(t: number, moves?: Moves): number;
85
103
  /** The last moment, never after `t`, at which the value may have become `at(t)`: `t` while it moves, `-Infinity` if it always was. */
86
- lastChange(t: number): number;
104
+ lastChange(t: number, moves?: Moves): number;
87
105
  /** This animation with its motion taken out — a loop at its first value, a one-shot cut to its end — drawn in its place under reduced motion. */
88
106
  reduced(): Animation<T>;
89
107
  }
@@ -99,8 +117,6 @@ export type Animation<T> = Timed<T> & ([T] extends [Renderable] ? Renderable & M
99
117
  /** What a frame drawn at a time shows of an animation, and what that says. */
100
118
  export interface Shown<T> {
101
119
  readonly value: T;
102
- /** The first moment, never before the time drawn at, the value may stop being this. */
103
- readonly nextChange: number;
104
120
  /** The animation the value is drawn from: the one handed in, or under reduced motion its reduced form. */
105
121
  readonly drawn: Timed<T>;
106
122
  /** Whether it is the animation reduced: drawn under reduced motion, changing only where content does. */
@@ -115,8 +131,8 @@ export interface Shown<T> {
115
131
  * reduces every one.
116
132
  */
117
133
  export declare function shownAt<T>(animation: Timed<T>, t: number, motion: Motion): Shown<T>;
118
- /** The hold `shown`, drawn at `t`, is in: its animation's two answers there. */
119
- export declare function holdOf(shown: Shown<unknown>, t: number): Hold;
134
+ /** The hold `shown`, drawn at `t`, is in: its animation's two answers there, given what its drawing said. */
135
+ export declare function holdOf(shown: Shown<unknown>, t: number, moves?: Moves): Hold;
120
136
  /** The style saying a cell is in `hold`. */
121
137
  export declare function stamp(hold: Hold): Style;
122
138
  /**
@@ -138,17 +154,20 @@ export declare function stampFrame(segments: Iterable<Segment>, hold: Hold): Ite
138
154
  export declare function frameTimeOf(options: RenderOptions): number;
139
155
  /**
140
156
  * An animation placed in the frame `options` draws: its value at the frame's
141
- * time (`frameTimeOf`, `shownAt`), drawn by `draw`, every segment stamped
142
- * with its hold there under whatever its own cells say, and the hold told to
143
- * the frame (`RenderOptions.onHold`), because a frame of no cells — an
144
- * element not yet entered — may have cells later, and must say when without
145
- * taking a row to say it in. [LAW:single-enforcer] The one placement: an
146
- * animation of renderables (`placedRender`) and a widget drawing one cell
147
- * from an animation of its own (a caret's blink) place through it, so how a
148
- * time reaches an animation and how its hold reaches the frame is written
149
- * once.
157
+ * time (`frameTimeOf`, `shownAt`), drawn whole by `draw` under the options it
158
+ * is handed, every segment then stamped with its hold there under whatever
159
+ * its own cells say, and the hold told to the frame (`RenderOptions.onHold`),
160
+ * because a frame of no cells — an element not yet entered — may have cells
161
+ * later, and must say when without taking a row to say it in. Drawn first,
162
+ * because the hold is asked with what the drawing said (`Moves`); each report
163
+ * goes on outward too, since a value drawn inside a placement in this value
164
+ * may be a part of an animation placed further out. [LAW:single-enforcer]
165
+ * The one placement: an animation of renderables (`placedRender`), an effect
166
+ * and a widget drawing one cell from an animation of its own (a caret's
167
+ * blink) place through it, so how a time reaches an animation and how its
168
+ * hold reaches the frame is written once.
150
169
  */
151
- export declare function placed<T>(animation: Timed<T>, options: RenderOptions, draw: (value: T) => Iterable<Segment>): Iterable<Segment>;
170
+ export declare function placed<T>(animation: Timed<T>, options: RenderOptions, draw: (value: T, options: RenderOptions) => Iterable<Segment>): Iterable<Segment>;
152
171
  /**
153
172
  * An animation's face, `render`: `placed`, its value drawn as the renderable
154
173
  * it is. A container — the library's or a user's — that draws its children
@@ -9,7 +9,8 @@
9
9
  * changes every frame, however close together they are drawn. An early
10
10
  * answer costs a host a frame that draws what the last one did; a late one
11
11
  * skips a change. So an animation that cannot say answers `t`, as
12
- * `continuous` does. A value drawn as cells carries this answer on every
12
+ * `continuous` does. An answer that depends on what the frame drew, an
13
+ * effect's, is told by that drawing (`Moves`). A value drawn as cells carries this answer on every
13
14
  * cell (`Style.hold`, both ends of the hold below), so a host holding
14
15
  * the composed frame reads it there (`Segment.nextChange`); a value that is
15
16
  * not drawn — a number, a colour — has `nextChange` as its only way to say
@@ -76,7 +77,7 @@ import { beat, firstReaching, passAt, positiveSeconds } from "./easing.js";
76
77
  import { Measurement } from "./measure.js";
77
78
  import { isRenderable, render } from "./protocol.js";
78
79
  import { Segment } from "./segment.js";
79
- import { Style } from "./style.js";
80
+ import { intersection, NEVER_CHANGES, Style } from "./style.js";
80
81
  /**
81
82
  * What a frame drawn at `t` shows of `animation` under `motion`, and when
82
83
  * that changes: the animation's, or under reduced motion its reduced form's
@@ -88,11 +89,11 @@ import { Style } from "./style.js";
88
89
  export function shownAt(animation, t, motion) {
89
90
  const reduced = motion.reducedMotion === true;
90
91
  const drawn = reduced ? animation.reduced() : animation;
91
- return { value: drawn.at(t), nextChange: drawn.nextChange(t), drawn, reduced };
92
+ return { value: drawn.at(t), drawn, reduced };
92
93
  }
93
- /** The hold `shown`, drawn at `t`, is in: its animation's two answers there. */
94
- export function holdOf(shown, t) {
95
- return { lastChange: shown.drawn.lastChange(t), nextChange: shown.nextChange };
94
+ /** The hold `shown`, drawn at `t`, is in: its animation's two answers there, given what its drawing said. */
95
+ export function holdOf(shown, t, moves) {
96
+ return { lastChange: shown.drawn.lastChange(t, moves), nextChange: shown.drawn.nextChange(t, moves) };
96
97
  }
97
98
  /** The style saying a cell is in `hold`. */
98
99
  export function stamp(hold) {
@@ -127,22 +128,33 @@ function drawable(value) {
127
128
  }
128
129
  /**
129
130
  * An animation placed in the frame `options` draws: its value at the frame's
130
- * time (`frameTimeOf`, `shownAt`), drawn by `draw`, every segment stamped
131
- * with its hold there under whatever its own cells say, and the hold told to
132
- * the frame (`RenderOptions.onHold`), because a frame of no cells — an
133
- * element not yet entered — may have cells later, and must say when without
134
- * taking a row to say it in. [LAW:single-enforcer] The one placement: an
135
- * animation of renderables (`placedRender`) and a widget drawing one cell
136
- * from an animation of its own (a caret's blink) place through it, so how a
137
- * time reaches an animation and how its hold reaches the frame is written
138
- * once.
131
+ * time (`frameTimeOf`, `shownAt`), drawn whole by `draw` under the options it
132
+ * is handed, every segment then stamped with its hold there under whatever
133
+ * its own cells say, and the hold told to the frame (`RenderOptions.onHold`),
134
+ * because a frame of no cells — an element not yet entered — may have cells
135
+ * later, and must say when without taking a row to say it in. Drawn first,
136
+ * because the hold is asked with what the drawing said (`Moves`); each report
137
+ * goes on outward too, since a value drawn inside a placement in this value
138
+ * may be a part of an animation placed further out. [LAW:single-enforcer]
139
+ * The one placement: an animation of renderables (`placedRender`), an effect
140
+ * and a widget drawing one cell from an animation of its own (a caret's
141
+ * blink) place through it, so how a time reaches an animation and how its
142
+ * hold reaches the frame is written once.
139
143
  */
140
144
  export function* placed(animation, options, draw) {
141
145
  const t = frameTimeOf(options);
142
146
  const shown = shownAt(animation, t, options);
143
- const hold = holdOf(shown, t);
147
+ const said = new Map();
148
+ const part = (at, key) => `${at} ${key}`;
149
+ const onMoved = (at, key, hold) => {
150
+ const known = said.get(part(at, key));
151
+ said.set(part(at, key), known === undefined ? hold : intersection(known, hold));
152
+ options.onMoved?.(at, key, hold);
153
+ };
154
+ const segments = [...draw(shown.value, { ...options, onMoved })];
155
+ const hold = holdOf(shown, t, (at, key) => said.get(part(at, key)));
144
156
  options.onHold?.(hold);
145
- yield* stampFrame(draw(shown.value), hold);
157
+ yield* stampFrame(segments, hold);
146
158
  }
147
159
  /**
148
160
  * An animation's face, `render`: `placed`, its value drawn as the renderable
@@ -151,7 +163,7 @@ export function* placed(animation, options, draw) {
151
163
  * the hold goes back the way the time came in. [LAW:composability]
152
164
  */
153
165
  export function placedRender(animation, options) {
154
- return placed(animation, options, (value) => render(drawable(value), options));
166
+ return placed(animation, options, (value, drawing) => render(drawable(value), drawing));
155
167
  }
156
168
  /**
157
169
  * An animation's face, `measure`: what it shows at the frame's time, measured,
@@ -161,9 +173,16 @@ export function placedRender(animation, options) {
161
173
  export function placedMeasure(animation, options) {
162
174
  const t = frameTimeOf(options);
163
175
  const shown = shownAt(animation, t, options);
164
- options.onHold?.(holdOf(shown, t));
176
+ options.onHold?.(holdOf(shown, t, UNLAID));
165
177
  return Measurement.get(options, drawable(shown.value));
166
178
  }
179
+ /**
180
+ * What a measurement's drawing would say: held for good. An effect changes
181
+ * how cells look and never which exist, so nothing it does can change a
182
+ * layout; a combinator's own switches and a child's holds still reach the
183
+ * measurement. [LAW:types-are-the-program]
184
+ */
185
+ const UNLAID = () => NEVER_CHANGES;
167
186
  /**
168
187
  * Every member of `source`, its own and its class's, as own properties of a
169
188
  * new object: a method bound to `source`, a getter read off it live, a field
@@ -258,8 +277,8 @@ export const Animation = {
258
277
  map(animation, f) {
259
278
  return Animation.of({
260
279
  at: (t) => f(animation.at(t)),
261
- nextChange: (t) => animation.nextChange(t),
262
- lastChange: (t) => animation.lastChange(t),
280
+ nextChange: (t, moves) => animation.nextChange(t, moves),
281
+ lastChange: (t, moves) => animation.lastChange(t, moves),
263
282
  reduced: () => Animation.map(animation.reduced(), f),
264
283
  });
265
284
  },
@@ -276,8 +295,8 @@ export const Animation = {
276
295
  at: (t) => animations.map((animation) => animation.at(t)),
277
296
  // A fold, not a spread: a stagger over a table's rows is a list as long
278
297
  // as the table, past what an engine takes as arguments.
279
- nextChange: (t) => animations.reduce((soonest, animation) => Math.min(soonest, animation.nextChange(t)), Number.POSITIVE_INFINITY),
280
- lastChange: (t) => animations.reduce((latest, animation) => Math.max(latest, animation.lastChange(t)), Number.NEGATIVE_INFINITY),
298
+ nextChange: (t, moves) => animations.reduce((soonest, animation) => Math.min(soonest, animation.nextChange(t, moves)), Number.POSITIVE_INFINITY),
299
+ lastChange: (t, moves) => animations.reduce((latest, animation) => Math.max(latest, animation.lastChange(t, moves)), Number.NEGATIVE_INFINITY),
281
300
  reduced: () => Animation.all(animations.map((animation) => animation.reduced())),
282
301
  });
283
302
  },
@@ -296,8 +315,8 @@ export const Animation = {
296
315
  onBeat(animation, beat) {
297
316
  return Animation.of({
298
317
  at: (t) => animation.at(beat.lastChange(t)),
299
- nextChange(t) {
300
- const moved = animation.nextChange(beat.lastChange(t));
318
+ nextChange(t, moves) {
319
+ const moved = animation.nextChange(beat.lastChange(t), moves);
301
320
  if (moved === Number.POSITIVE_INFINITY)
302
321
  return moved;
303
322
  const end = beat.nextChange(t);
@@ -306,8 +325,13 @@ export const Animation = {
306
325
  return moved <= end ? end : beat.lastChange(moved) === moved ? moved : beat.nextChange(moved);
307
326
  },
308
327
  // The value held now began at this hold's start at the latest: an early
309
- // answer where the animation had not moved by it, which the contract allows.
310
- lastChange: (t) => beat.lastChange(t),
328
+ // answer where the animation had not moved by it, which the contract
329
+ // allows. One that always was never turned on any beat.
330
+ lastChange(t, moves) {
331
+ const began = beat.lastChange(t);
332
+ const moved = animation.lastChange(began, moves);
333
+ return moved === Number.NEGATIVE_INFINITY ? moved : began;
334
+ },
311
335
  // Reduced, nothing moves between beats, so there is nothing to sample on them.
312
336
  reduced: () => animation.reduced(),
313
337
  });
@@ -331,19 +355,19 @@ export const Animation = {
331
355
  const carried = (local) => (Number.isFinite(local) ? firstReaching(local + seconds, (x) => x - seconds >= local) : local);
332
356
  return Animation.of({
333
357
  at: (t) => (t - seconds >= 0 ? then.at(t - seconds) : first.at(t)),
334
- nextChange(t) {
358
+ nextChange(t, moves) {
335
359
  const into = t - seconds;
336
360
  if (into < 0)
337
- return Math.min(first.nextChange(t), seconds);
338
- return Math.max(t, carried(then.nextChange(into)));
361
+ return Math.min(first.nextChange(t, moves), seconds);
362
+ return Math.max(t, carried(then.nextChange(into, moves)));
339
363
  },
340
364
  // After the switch the value became what it is at the switch or when
341
365
  // `then`'s did since, whichever is later; before it, when `first`'s did.
342
- lastChange(t) {
366
+ lastChange(t, moves) {
343
367
  const into = t - seconds;
344
368
  if (into < 0)
345
- return first.lastChange(t);
346
- return Math.min(t, Math.max(seconds, carried(then.lastChange(into))));
369
+ return first.lastChange(t, moves);
370
+ return Math.min(t, Math.max(seconds, carried(then.lastChange(into, moves))));
347
371
  },
348
372
  reduced: () => Animation.sequence(first.reduced(), seconds, then.reduced()),
349
373
  });
@@ -381,14 +405,14 @@ export const Animation = {
381
405
  const { pass, into } = passAt("Animation.repeat", t, period);
382
406
  return nth(pass).at(into);
383
407
  },
384
- nextChange(t) {
408
+ nextChange(t, moves) {
385
409
  const { pass, into } = passAt("Animation.repeat", t, period);
386
- return Math.max(t, Math.min(turns.nextChange(t), carried(pass, turns.lastChange(t), nth(pass).nextChange(into))));
410
+ return Math.max(t, Math.min(turns.nextChange(t), carried(pass, turns.lastChange(t), nth(pass).nextChange(into, moves))));
387
411
  },
388
- lastChange(t) {
412
+ lastChange(t, moves) {
389
413
  const { pass, into } = passAt("Animation.repeat", t, period);
390
414
  const began = turns.lastChange(t);
391
- return Math.min(t, Math.max(began, carried(pass, began, nth(pass).lastChange(into))));
415
+ return Math.min(t, Math.max(began, carried(pass, began, nth(pass).lastChange(into, moves))));
392
416
  },
393
417
  reduced: () => Animation.repeat(period, (n) => nth(n).reduced()),
394
418
  });
@@ -21,18 +21,25 @@
21
21
  *
22
22
  * Carriage returns are the other departure, and the same argument. Rich splits
23
23
  * lines with Python's `splitlines`, which also breaks at `\r`, so its `a\rb`
24
- * is two lines. Here a line ends only at `\n`, and `\r` returns to the first
25
- * column, so later text overwrites earlier text character by character:
26
- * `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
- * is still `done`. Columns are counted in characters, not cells, so a wide
28
- * character overwritten by a narrow one is where this and a terminal differ.
29
- * Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
24
+ * is two lines. Here a line ends at `\n` and the census's other line breaks, and
25
+ * `\r` returns to the first column, so later text overwrites earlier text cell by
26
+ * cell: `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
+ * is still `done`. Columns are a terminal's cells: a wide character takes two, a
28
+ * combining mark none, a tab moves to the next stop over what is there, and a wide
29
+ * character or a tab half overwritten leaves blanks. Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
30
30
  * is usually `\r\x1b[K` and the old text must not show through a shorter new
31
31
  * one.
32
32
  *
33
+ * Backspace is the third. Rich drops it; here it is nroff's overstrike, the
34
+ * adjacent `x BS x` bold and `_ BS x` underline that `man` writes, and a bare one
35
+ * moves back a column, so `12%\b\b\b13%` is the redraw it looks like. The bytes
36
+ * alone cannot tell `1\b1` the redraw from `1\b1` the bold, and `less` reads them
37
+ * as overstrike with no other evidence, so this does.
38
+ *
33
39
  * Everything else follows Rich: malformed SGR parameters are skipped rather
34
- * than refused, and every other escape is dropped. It is not a terminal
35
- * emulator — any other cursor movement is dropped, not performed.
40
+ * than refused, and every other escape is dropped, with every control a terminal
41
+ * draws nothing for. It is not a terminal emulator — any other cursor movement is
42
+ * dropped, not performed.
36
43
  *
37
44
  * Tier 5 of `src/core/`: it builds `RichText`, so it sits above `text`, and
38
45
  * `RichText` cannot offer a `fromAnsi` static without importing upward.
package/dist/core/ansi.js CHANGED
@@ -21,48 +21,78 @@
21
21
  *
22
22
  * Carriage returns are the other departure, and the same argument. Rich splits
23
23
  * lines with Python's `splitlines`, which also breaks at `\r`, so its `a\rb`
24
- * is two lines. Here a line ends only at `\n`, and `\r` returns to the first
25
- * column, so later text overwrites earlier text character by character:
26
- * `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
- * is still `done`. Columns are counted in characters, not cells, so a wide
28
- * character overwritten by a narrow one is where this and a terminal differ.
29
- * Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
24
+ * is two lines. Here a line ends at `\n` and the census's other line breaks, and
25
+ * `\r` returns to the first column, so later text overwrites earlier text cell by
26
+ * cell: `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
+ * is still `done`. Columns are a terminal's cells: a wide character takes two, a
28
+ * combining mark none, a tab moves to the next stop over what is there, and a wide
29
+ * character or a tab half overwritten leaves blanks. Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
30
30
  * is usually `\r\x1b[K` and the old text must not show through a shorter new
31
31
  * one.
32
32
  *
33
+ * Backspace is the third. Rich drops it; here it is nroff's overstrike, the
34
+ * adjacent `x BS x` bold and `_ BS x` underline that `man` writes, and a bare one
35
+ * moves back a column, so `12%\b\b\b13%` is the redraw it looks like. The bytes
36
+ * alone cannot tell `1\b1` the redraw from `1\b1` the bold, and `less` reads them
37
+ * as overstrike with no other evidence, so this does.
38
+ *
33
39
  * Everything else follows Rich: malformed SGR parameters are skipped rather
34
- * than refused, and every other escape is dropped. It is not a terminal
35
- * emulator — any other cursor movement is dropped, not performed.
40
+ * than refused, and every other escape is dropped, with every control a terminal
41
+ * draws nothing for. It is not a terminal emulator — any other cursor movement is
42
+ * dropped, not performed.
36
43
  *
37
44
  * Tier 5 of `src/core/`: it builds `RichText`, so it sits above `text`, and
38
45
  * `RichText` cannot offer a `fromAnsi` static without importing upward.
39
46
  */
47
+ import { cellLen, graphemes } from "./cells.js";
40
48
  import { ColorSpec } from "./color.js";
49
+ import { controlClass } from "./control-codes.js";
41
50
  import { osc8Sequences } from "./osc8.js";
42
51
  import { ATTRIBUTE_NAMES, ATTRIBUTE_SGR, NULL_STYLE, Style } from "./style.js";
43
52
  import { RichText } from "./text.js";
53
+ const BOLD = new Style({ bold: true });
54
+ const UNDERLINED = new Style({ underline: true });
55
+ const BOLD_UNDERLINED = BOLD.add(UNDERLINED);
56
+ const STRING_ESCAPE = String.raw `\x1b[\]P_^X][^\n]*?(?:\x07|\x1b\\|\x9c|\n|$)`;
44
57
  /**
45
58
  * Every escape but OSC 8, which `osc8Sequences` reads before this runs. In
46
- * order: a carriage return; an SGR sequence (group 1, its parameters — a
47
- * private marker such as the `>` of `\x1b[>4;2m` makes it some other CSI);
48
- * erase in line (group 2, its mode); any other CSI sequence; any other string
49
- * escape — OSC, DCS, APC, PM, SOS — run to the terminators OSC 8 accepts or
50
- * cut off by the end of the line; any other escape, ECMA-48's intermediates
51
- * and one final byte (`\x1b(B`, `\x1b)0`, `\x1b#8`, `\x1bc`). Only the first
52
- * three become tokens.
59
+ * order: a carriage return; an SGR sequence (`sgr`, its parameters — a private
60
+ * marker such as the `>` of `\x1b[>4;2m` makes it some other CSI); erase in line
61
+ * (`mode`); any other CSI sequence; any other string escape — OSC, DCS, APC, PM,
62
+ * SOS — run to the terminators OSC 8 accepts or cut off by the end of the line;
63
+ * any other escape, ECMA-48's intermediates and one final byte (`\x1b(B`,
64
+ * `\x1b)0`, `\x1b#8`, `\x1bc`); a character struck over itself (`struck`), over an
65
+ * underscore first or not (`underscore`); a character struck over an underscore
66
+ * alone (`underlined`); a backspace; and every control a line may not hold or a
67
+ * terminal draws nothing for. The silent ones are the two "any other" escapes and
68
+ * that last class.
53
69
  */
54
- const escapes = () => /\r|\x1b\[([0-9;:]*)m|\x1b\[([012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[\]P_^X][\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[ -/]*[0-~]/g;
70
+ const ESCAPES = [
71
+ String.raw `\r|\x1b\[(?<sgr>[0-9;:]*)m|\x1b\[(?<mode>[012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|${STRING_ESCAPE}|\x1b[ -/]*[0-~]`,
72
+ String.raw `(?<underscore>_\x08)?(?<struck>\P{Cc})(?:\x08\k<struck>)+|_\x08(?<underlined>\P{Cc})|\x08`,
73
+ controlClass((disposition) => disposition !== "keep" && disposition !== "withheld"),
74
+ ].join("|");
75
+ const escapes = () => new RegExp(ESCAPES, "gu");
76
+ // A string escape goes first so a line break in its payload stays in the payload.
77
+ const lineBreaks = () => new RegExp(`${STRING_ESCAPE}|${controlClass((disposition) => disposition === "line-break")}`, "gu");
55
78
  function* escapeTokens(bytes) {
56
79
  let at = 0;
57
80
  for (const match of bytes.matchAll(escapes())) {
81
+ const { sgr, mode, underscore, struck, underlined } = match.groups;
58
82
  if (match.index > at)
59
83
  yield { kind: "text", text: bytes.slice(at, match.index) };
60
84
  if (match[0] === "\r")
61
85
  yield { kind: "return" };
62
- if (match[1] !== undefined)
63
- yield { kind: "sgr", params: match[1] };
64
- if (match[2] !== undefined)
65
- yield { kind: "erase", mode: (match[2] || "0") };
86
+ if (sgr !== undefined)
87
+ yield { kind: "sgr", params: sgr };
88
+ if (mode !== undefined)
89
+ yield { kind: "erase", mode: (mode || "0") };
90
+ if (struck !== undefined)
91
+ yield { kind: "overstrike", character: struck, emphasis: underscore === undefined ? BOLD : BOLD_UNDERLINED };
92
+ if (underlined !== undefined)
93
+ yield { kind: "overstrike", character: underlined, emphasis: UNDERLINED };
94
+ if (match[0] === "\b")
95
+ yield { kind: "backspace" };
66
96
  at = match.index + match[0].length;
67
97
  }
68
98
  if (at < bytes.length)
@@ -153,11 +183,52 @@ function applySgr(style, params) {
153
183
  result = (SGR_OPS.get(code) ?? identity)(result, codes);
154
184
  return result;
155
185
  }
186
+ const ORIGIN = ["", NULL_STYLE];
156
187
  const BLANK = [" ", NULL_STYLE];
188
+ const TAB_STOP = 8;
189
+ /** The column holding the cluster drawn across `column`. */
190
+ function leadOf(cells, column) {
191
+ let at = column;
192
+ while (at > 0 && cells[at][0] === "")
193
+ at -= 1;
194
+ return at;
195
+ }
196
+ /** A cluster across the edge before `column` goes blank, as on a terminal. */
197
+ function splitWide(cells, column) {
198
+ if (cells[column]?.[0] !== "")
199
+ return;
200
+ const lead = leadOf(cells, column);
201
+ for (let at = lead; at === lead || cells[at]?.[0] === ""; at += 1)
202
+ cells[at] = BLANK;
203
+ }
204
+ /** `cluster` drawn at `column`, and the columns the cursor moves. */
205
+ function draw(cells, column, cluster, style) {
206
+ const width = cluster === "\t" ? TAB_STOP - ((column - 1) % TAB_STOP) : cellLen(cluster);
207
+ if (width === 0) {
208
+ const lead = leadOf(cells, column - 1);
209
+ cells[lead] = [cells[lead][0] + cluster, cells[lead][1]];
210
+ return 0;
211
+ }
212
+ if (cluster === "\t" && column < cells.length) {
213
+ while (cells.length < column + width)
214
+ cells.push(BLANK);
215
+ return width;
216
+ }
217
+ splitWide(cells, column);
218
+ splitWide(cells, column + width);
219
+ cells[column] = [cells[0][0] + cluster, style];
220
+ cells[0] = ORIGIN;
221
+ for (let at = 1; at < width; at += 1)
222
+ cells[column + at] = ["", style];
223
+ return width;
224
+ }
157
225
  /** Erase in line: the cells before the cursor go blank, the ones from it on go. */
158
226
  function erase(cells, column, mode) {
227
+ splitWide(cells, column);
228
+ splitWide(cells, column + 1);
229
+ cells[0] = ORIGIN;
159
230
  if (mode !== "0")
160
- cells.fill(BLANK, 0, Math.min(column + 1, cells.length));
231
+ cells.fill(BLANK, 1, Math.min(column + 1, cells.length));
161
232
  if (mode !== "1")
162
233
  cells.length = Math.min(column, cells.length);
163
234
  }
@@ -173,25 +244,32 @@ export class AnsiDecoder {
173
244
  link = undefined;
174
245
  /** One `RichText` per line of `ansi`; a final newline ends a line rather than starting one. */
175
246
  decode(ansi) {
176
- return (ansi.match(/[^\n]*\n|[^\n]+/g) ?? []).map((line) => this.decodeLine(line.replace(/\r?\n$/, "")));
247
+ const lined = ansi.replace(lineBreaks(), (found) => (found.startsWith("\x1b") ? found : "\n"));
248
+ return (lined.match(/[^\n]*\n|[^\n]+/g) ?? []).map((line) => this.decodeLine(line.replace(/\r?\n$/, "")));
177
249
  }
178
250
  /**
179
251
  * One line of output, holding no `\n`, written the way a terminal writes
180
252
  * it: `\r` returns to the first column, and text overwrites from there.
181
253
  */
182
254
  decodeLine(line) {
183
- const cells = [];
184
- let column = 0;
255
+ const cells = [ORIGIN];
256
+ let column = 1;
185
257
  for (const token of tokens(line)) {
186
258
  switch (token.kind) {
187
259
  case "text": {
188
260
  const style = this.sgr.withLink(this.link);
189
- for (const char of token.text)
190
- cells[column++] = [char, style];
261
+ for (const cluster of graphemes(token.text))
262
+ column += draw(cells, column, cluster, style);
191
263
  break;
192
264
  }
265
+ case "overstrike":
266
+ column += draw(cells, column, token.character, this.sgr.add(token.emphasis).withLink(this.link));
267
+ break;
193
268
  case "return":
194
- column = 0;
269
+ column = 1;
270
+ break;
271
+ case "backspace":
272
+ column = Math.max(1, column - 1);
195
273
  break;
196
274
  case "erase":
197
275
  erase(cells, column, token.mode);
@@ -205,7 +283,7 @@ export class AnsiDecoder {
205
283
  }
206
284
  }
207
285
  const runs = [];
208
- for (const [char, style] of cells) {
286
+ for (const [char, style] of cells.filter(([char]) => char !== "")) {
209
287
  const last = runs.at(-1);
210
288
  if (last?.[1].equals(style))
211
289
  last[0] += char;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The Unicode properties UAX #9 and Arabic joining read. Generated by
3
+ * scripts/bidi-data.py from the Unicode 17.0.0 Character Database; regenerate
4
+ * rather than edit.
5
+ */
6
+ /** The bidi classes other than L, in the order `CLASS_RANGES` numbers them. */
7
+ export declare const BIDI_CLASSES: readonly ["R", "AL", "EN", "ES", "ET", "AN", "CS", "NSM", "BN", "B", "S", "WS", "ON", "LRE", "LRO", "RLE", "RLO", "PDF", "LRI", "RLI", "FSI", "PDI"];
8
+ /** The joining types other than U, in the order `JOINING_RANGES` numbers them. */
9
+ export declare const JOINING_TYPES: readonly ["R", "L", "D", "C", "T"];
10
+ export declare const CLASS_RANGES: readonly (readonly [number, number, number])[];
11
+ export declare const JOINING_RANGES: readonly (readonly [number, number, number])[];
12
+ export declare const BRACKET_PAIRS: readonly (readonly [number, number])[];
13
+ export declare const MIRRORS: readonly (readonly [number, number])[];
14
+ export declare const PRESENTATION_FORMS: readonly (readonly [number, number, number, number, number])[];