@promptctl/rich-js 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/console.js +5 -5
  9. package/dist/core/easing.d.ts +110 -10
  10. package/dist/core/easing.js +269 -36
  11. package/dist/core/json.js +2 -1
  12. package/dist/core/measure.js +2 -2
  13. package/dist/core/place.js +2 -1
  14. package/dist/core/pretty.js +2 -2
  15. package/dist/core/protocol.d.ts +73 -19
  16. package/dist/core/protocol.js +56 -15
  17. package/dist/core/render.js +2 -1
  18. package/dist/core/segment.d.ts +18 -1
  19. package/dist/core/segment.js +45 -44
  20. package/dist/core/strip.d.ts +3 -2
  21. package/dist/core/strip.js +11 -7
  22. package/dist/core/style.js +17 -6
  23. package/dist/core/text.js +22 -14
  24. package/dist/core/timeline.d.ts +84 -0
  25. package/dist/core/timeline.js +144 -0
  26. package/dist/core/tween.d.ts +49 -0
  27. package/dist/core/tween.js +69 -0
  28. package/dist/host/app.js +2 -2
  29. package/dist/index.d.ts +10 -7
  30. package/dist/index.js +13 -4
  31. package/dist/renderables/align.d.ts +3 -2
  32. package/dist/renderables/align.js +3 -0
  33. package/dist/renderables/columns.d.ts +3 -2
  34. package/dist/renderables/columns.js +7 -2
  35. package/dist/renderables/constrain.d.ts +3 -2
  36. package/dist/renderables/constrain.js +5 -1
  37. package/dist/renderables/effect.d.ts +43 -58
  38. package/dist/renderables/effect.js +58 -64
  39. package/dist/renderables/effects.d.ts +16 -14
  40. package/dist/renderables/effects.js +64 -45
  41. package/dist/renderables/embed.js +3 -2
  42. package/dist/renderables/flexStrip.d.ts +3 -2
  43. package/dist/renderables/flexStrip.js +6 -1
  44. package/dist/renderables/frames.d.ts +40 -19
  45. package/dist/renderables/frames.js +48 -21
  46. package/dist/renderables/group.d.ts +3 -2
  47. package/dist/renderables/group.js +5 -2
  48. package/dist/renderables/html-entities.d.ts +8 -0
  49. package/dist/renderables/html-entities.js +386 -0
  50. package/dist/renderables/layout.d.ts +3 -2
  51. package/dist/renderables/layout.js +15 -2
  52. package/dist/renderables/list-view.d.ts +3 -2
  53. package/dist/renderables/list-view.js +12 -5
  54. package/dist/renderables/live.js +2 -2
  55. package/dist/renderables/markdown-parse.d.ts +109 -0
  56. package/dist/renderables/markdown-parse.js +1421 -0
  57. package/dist/renderables/markdown-url.d.ts +19 -0
  58. package/dist/renderables/markdown-url.js +314 -0
  59. package/dist/renderables/markdown.d.ts +5 -7
  60. package/dist/renderables/markdown.js +356 -683
  61. package/dist/renderables/padding.d.ts +8 -20
  62. package/dist/renderables/padding.js +32 -50
  63. package/dist/renderables/panel.d.ts +26 -26
  64. package/dist/renderables/panel.js +67 -103
  65. package/dist/renderables/progress.d.ts +4 -2
  66. package/dist/renderables/progress.js +11 -2
  67. package/dist/renderables/progressBar.js +2 -2
  68. package/dist/renderables/python-space.d.ts +13 -0
  69. package/dist/renderables/python-space.js +24 -0
  70. package/dist/renderables/spinner.d.ts +26 -14
  71. package/dist/renderables/spinner.js +49 -37
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +19 -11
  74. package/dist/renderables/table.js +140 -156
  75. package/dist/renderables/traceback.d.ts +1 -1
  76. package/dist/renderables/traceback.js +6 -5
  77. package/dist/renderables/tree.d.ts +6 -5
  78. package/dist/renderables/tree.js +11 -7
  79. package/dist/renderables/viewport.d.ts +13 -5
  80. package/dist/renderables/viewport.js +41 -20
  81. package/dist/template-bindings/animation-funcs.js +2 -2
  82. package/dist/template-bindings/index.js +2 -1
  83. package/dist/themes/ramp.d.ts +18 -3
  84. package/dist/themes/ramp.js +23 -2
  85. package/dist/widgets/focus-manager.d.ts +21 -14
  86. package/dist/widgets/focus-manager.js +28 -29
  87. package/dist/widgets/index.d.ts +1 -1
  88. package/dist/widgets/static-item.d.ts +4 -3
  89. package/dist/widgets/static-item.js +9 -7
  90. package/dist/widgets/text-input.d.ts +28 -18
  91. package/dist/widgets/text-input.js +144 -101
  92. package/dist/widgets/widget-app.d.ts +19 -3
  93. package/dist/widgets/widget-app.js +39 -21
  94. package/dist/widgets/widget-base.js +0 -3
  95. package/package.json +2 -5
@@ -1,6 +1,6 @@
1
1
  Third-party notices for @promptctl/rich-js
2
2
 
3
- This package contains material from the three projects below. All are
3
+ This package contains material from the four projects below. All are
4
4
  distributed under the MIT License, which requires their copyright and
5
5
  permission notices to travel with every copy; they are reproduced here
6
6
  unchanged. The license for rich-js itself is in LICENSE.
@@ -79,3 +79,31 @@ The above copyright notice and this permission notice shall be included in all c
79
79
 
80
80
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
81
81
 
82
+ -------------------------------------------------------------------------------
83
+ wcwidth
84
+ https://github.com/jquast/wcwidth
85
+
86
+ The cell width of each code point is Rich's table, which Rich generates from
87
+ wcwidth's data.
88
+
89
+ The MIT License (MIT)
90
+
91
+ Copyright (c) 2014 Jeff Quast <contact@jeffquast.com>
92
+
93
+ Permission is hereby granted, free of charge, to any person obtaining a copy
94
+ of this software and associated documentation files (the "Software"), to deal
95
+ in the Software without restriction, including without limitation the rights
96
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
97
+ copies of the Software, and to permit persons to whom the Software is
98
+ furnished to do so, subject to the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be included in all
101
+ copies or substantial portions of the Software.
102
+
103
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
104
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
105
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
106
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
107
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
108
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
109
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Animation — a value for a time, and when it next changes: the one type
3
+ * anything in rich-js that changes with time is an instance of.
4
+ *
5
+ * `at(t)` is the value at `t` seconds. `nextChange(t)` is the first moment,
6
+ * on `t`'s own timeline, at which the value may stop being `at(t)`: never
7
+ * before `t`. A later time says the value is held until then, `Infinity`
8
+ * that it never changes again, and `t` itself that it is moving at `t` — it
9
+ * changes every frame, however close together they are drawn. An early
10
+ * answer costs a host a frame that draws what the last one did; a late one
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
13
+ * cell (`Style.nextChange`), so a host holding the composed frame reads it
14
+ * there (`Segment.nextChange`); a value that is not drawn — a number, a
15
+ * colour — has `nextChange` as its only way to say when to look again.
16
+ *
17
+ * `lastChange(t)` is the other end of the same hold: the last moment, never
18
+ * after `t`, at which the value may have become `at(t)` — `-Infinity` if it
19
+ * always was, `t` while it moves. Together they bound the hold, `[lastChange(t),
20
+ * nextChange(t))`, and the answer may be narrower than the truth and never
21
+ * wider. It is what a host running the animation backwards reads
22
+ * (`./timeline.ts`): run down the timeline, the value next changes where it
23
+ * began. An animation that cannot say answers `t` here too.
24
+ *
25
+ * The instances already in the tree: a `Beat` is an animation of which hold
26
+ * is in progress (`./easing.ts`), a `FrameSequence` of the frame shown and a
27
+ * `Spinner` of the spinner drawn (`../renderables/`), and an `Effect` is, for
28
+ * what an element draws in a frame, an animation of how its cells' colours
29
+ * move (`../renderables/effect.ts`). Every kind of motion is one more.
30
+ *
31
+ * [LAW:no-ambient-temporal-coupling] Pure: `t` is an argument and never a
32
+ * clock read (`./clock.ts`), the same `t` gives the same value, nothing is
33
+ * kept between calls, and a call costs the same at `t = 0` and `t = 10⁶`, so
34
+ * a host sampling once a second and one sampling sixty times see the same
35
+ * value at every `t` they share.
36
+ *
37
+ * [LAW:one-type-per-behavior] The combinators here are the ones every kind
38
+ * of motion is built from — a constant, a value moving every moment, a map
39
+ * over the value, several at once, and one animation after another — and each says how
40
+ * `nextChange` composes, so an animation built from them answers it without
41
+ * its author doing anything.
42
+ */
43
+ /** A value of `T` at a time in seconds, and the first moment that value may change. */
44
+ export interface Animation<T> {
45
+ /** The value at `t` seconds. */
46
+ at(t: number): T;
47
+ /** The first moment, never before `t`, at which the value may stop being `at(t)`: `t` while it moves, `Infinity` once it never will. */
48
+ nextChange(t: number): number;
49
+ /** The last moment, never after `t`, at which the value may have become `at(t)`: `t` while it moves, `-Infinity` if it always was. */
50
+ lastChange(t: number): number;
51
+ }
52
+ export declare const Animation: {
53
+ /** `value` at every time, never changing. */
54
+ constant<T>(value: T): Animation<T>;
55
+ /**
56
+ * The value `at` gives, taken to move at every moment: what an animation
57
+ * that cannot say when it holds still is, so a host draws it every frame.
58
+ */
59
+ continuous<T>(at: (t: number) => T): Animation<T>;
60
+ /**
61
+ * `animation`'s value through `f`, changing when it does. Two values `f`
62
+ * maps alike make an early answer, which the contract allows.
63
+ */
64
+ map<T, U>(animation: Animation<T>, f: (value: T) => U): Animation<U>;
65
+ /**
66
+ * Every one of `animations` at once, their values in order, changing when
67
+ * the soonest of them does — so a stack of effects drawn together holds
68
+ * still exactly while each of them does. None never changes.
69
+ */
70
+ all<T>(animations: readonly Animation<T>[]): Animation<T[]>;
71
+ /**
72
+ * `first` until `seconds`, then `then`, handed the time since it began —
73
+ * each part on its own clock from 0, as every effect is. Before the switch
74
+ * the value next changes when `first`'s does or at the switch, whichever is
75
+ * sooner; after it, when `then`'s does, carried back onto this timeline.
76
+ */
77
+ sequence<T>(first: Animation<T>, seconds: number, then: Animation<T>): Animation<T>;
78
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Animation — a value for a time, and when it next changes: the one type
3
+ * anything in rich-js that changes with time is an instance of.
4
+ *
5
+ * `at(t)` is the value at `t` seconds. `nextChange(t)` is the first moment,
6
+ * on `t`'s own timeline, at which the value may stop being `at(t)`: never
7
+ * before `t`. A later time says the value is held until then, `Infinity`
8
+ * that it never changes again, and `t` itself that it is moving at `t` — it
9
+ * changes every frame, however close together they are drawn. An early
10
+ * answer costs a host a frame that draws what the last one did; a late one
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
13
+ * cell (`Style.nextChange`), so a host holding the composed frame reads it
14
+ * there (`Segment.nextChange`); a value that is not drawn — a number, a
15
+ * colour — has `nextChange` as its only way to say when to look again.
16
+ *
17
+ * `lastChange(t)` is the other end of the same hold: the last moment, never
18
+ * after `t`, at which the value may have become `at(t)` — `-Infinity` if it
19
+ * always was, `t` while it moves. Together they bound the hold, `[lastChange(t),
20
+ * nextChange(t))`, and the answer may be narrower than the truth and never
21
+ * wider. It is what a host running the animation backwards reads
22
+ * (`./timeline.ts`): run down the timeline, the value next changes where it
23
+ * began. An animation that cannot say answers `t` here too.
24
+ *
25
+ * The instances already in the tree: a `Beat` is an animation of which hold
26
+ * is in progress (`./easing.ts`), a `FrameSequence` of the frame shown and a
27
+ * `Spinner` of the spinner drawn (`../renderables/`), and an `Effect` is, for
28
+ * what an element draws in a frame, an animation of how its cells' colours
29
+ * move (`../renderables/effect.ts`). Every kind of motion is one more.
30
+ *
31
+ * [LAW:no-ambient-temporal-coupling] Pure: `t` is an argument and never a
32
+ * clock read (`./clock.ts`), the same `t` gives the same value, nothing is
33
+ * kept between calls, and a call costs the same at `t = 0` and `t = 10⁶`, so
34
+ * a host sampling once a second and one sampling sixty times see the same
35
+ * value at every `t` they share.
36
+ *
37
+ * [LAW:one-type-per-behavior] The combinators here are the ones every kind
38
+ * of motion is built from — a constant, a value moving every moment, a map
39
+ * over the value, several at once, and one animation after another — and each says how
40
+ * `nextChange` composes, so an animation built from them answers it without
41
+ * its author doing anything.
42
+ */
43
+ import { firstReaching, positiveSeconds } from "./easing.js";
44
+ export const Animation = {
45
+ /** `value` at every time, never changing. */
46
+ constant(value) {
47
+ return { at: () => value, nextChange: () => Number.POSITIVE_INFINITY, lastChange: () => Number.NEGATIVE_INFINITY };
48
+ },
49
+ /**
50
+ * The value `at` gives, taken to move at every moment: what an animation
51
+ * that cannot say when it holds still is, so a host draws it every frame.
52
+ */
53
+ continuous(at) {
54
+ return { at, nextChange: (t) => t, lastChange: (t) => t };
55
+ },
56
+ /**
57
+ * `animation`'s value through `f`, changing when it does. Two values `f`
58
+ * maps alike make an early answer, which the contract allows.
59
+ */
60
+ map(animation, f) {
61
+ return { at: (t) => f(animation.at(t)), nextChange: (t) => animation.nextChange(t), lastChange: (t) => animation.lastChange(t) };
62
+ },
63
+ /**
64
+ * Every one of `animations` at once, their values in order, changing when
65
+ * the soonest of them does — so a stack of effects drawn together holds
66
+ * still exactly while each of them does. None never changes.
67
+ */
68
+ all(animations) {
69
+ return {
70
+ at: (t) => animations.map((animation) => animation.at(t)),
71
+ nextChange: (t) => Math.min(...animations.map((animation) => animation.nextChange(t))),
72
+ lastChange: (t) => Math.max(...animations.map((animation) => animation.lastChange(t))),
73
+ };
74
+ },
75
+ /**
76
+ * `first` until `seconds`, then `then`, handed the time since it began —
77
+ * each part on its own clock from 0, as every effect is. Before the switch
78
+ * the value next changes when `first`'s does or at the switch, whichever is
79
+ * sooner; after it, when `then`'s does, carried back onto this timeline.
80
+ */
81
+ sequence(first, seconds, then) {
82
+ positiveSeconds("Animation.sequence seconds", seconds);
83
+ // `then`'s moment `local`, on its own clock, carried back onto this
84
+ // timeline: the first moment whose time since the switch reaches
85
+ // `local`, which the sum `local + seconds` can round to either side of —
86
+ // under it, where `at` still shows the old value, or over it, a float
87
+ // after the change showed.
88
+ const carried = (local) => (Number.isFinite(local) ? firstReaching(local + seconds, (x) => x - seconds >= local) : local);
89
+ return {
90
+ at: (t) => (t - seconds >= 0 ? then.at(t - seconds) : first.at(t)),
91
+ nextChange(t) {
92
+ const into = t - seconds;
93
+ if (into < 0)
94
+ return Math.min(first.nextChange(t), seconds);
95
+ return Math.max(t, carried(then.nextChange(into)));
96
+ },
97
+ // After the switch the value became what it is at the switch or when
98
+ // `then`'s did since, whichever is later; before it, when `first`'s did.
99
+ lastChange(t) {
100
+ const into = t - seconds;
101
+ if (into < 0)
102
+ return first.lastChange(t);
103
+ return Math.min(t, Math.max(seconds, carried(then.lastChange(into))));
104
+ },
105
+ };
106
+ },
107
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The cells Rich's `cell_len` gives each code point, and the code points VS16
3
+ * widens. Generated by scripts/cell-widths.py from Rich 15.0.0's Unicode 17.0.0
4
+ * table, which Rich builds from wcwidth's data; regenerate rather than edit.
5
+ */
6
+ /** The cells Rich gives `codePoint` on its own: none for a C0 or C1 control. */
7
+ export declare function codePointWidth(codePoint: number): 0 | 1 | 2;
8
+ /** Whether VS16 after `codePoint` widens it from one cell to two. */
9
+ export declare function widensToEmoji(codePoint: number): boolean;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The cells Rich's `cell_len` gives each code point, and the code points VS16
3
+ * widens. Generated by scripts/cell-widths.py from Rich 15.0.0's Unicode 17.0.0
4
+ * table, which Rich builds from wcwidth's data; regenerate rather than edit.
5
+ */
6
+ // Every range of code points that does not take one cell, as [first, last,
7
+ // cells]: ascending, disjoint, adjacent ranges of one width merged.
8
+ const RANGES = [
9
+ [0x0, 0x0, 0], [0x300, 0x36f, 0], [0x483, 0x489, 0], [0x591, 0x5bd, 0], [0x5bf, 0x5bf, 0], [0x5c1, 0x5c2, 0],
10
+ [0x5c4, 0x5c5, 0], [0x5c7, 0x5c7, 0], [0x610, 0x61a, 0], [0x61c, 0x61c, 0], [0x64b, 0x65f, 0], [0x670, 0x670, 0],
11
+ [0x6d6, 0x6dc, 0], [0x6df, 0x6e4, 0], [0x6e7, 0x6e8, 0], [0x6ea, 0x6ed, 0], [0x711, 0x711, 0], [0x730, 0x74a, 0],
12
+ [0x7a6, 0x7b0, 0], [0x7eb, 0x7f3, 0], [0x7fd, 0x7fd, 0], [0x816, 0x819, 0], [0x81b, 0x823, 0], [0x825, 0x827, 0],
13
+ [0x829, 0x82d, 0], [0x859, 0x85b, 0], [0x897, 0x89f, 0], [0x8ca, 0x8e1, 0], [0x8e3, 0x903, 0], [0x93a, 0x93c, 0],
14
+ [0x93e, 0x94f, 0], [0x951, 0x957, 0], [0x962, 0x963, 0], [0x981, 0x983, 0], [0x9bc, 0x9bc, 0], [0x9be, 0x9c4, 0],
15
+ [0x9c7, 0x9c8, 0], [0x9cb, 0x9cd, 0], [0x9d7, 0x9d7, 0], [0x9e2, 0x9e3, 0], [0x9fe, 0x9fe, 0], [0xa01, 0xa03, 0],
16
+ [0xa3c, 0xa3c, 0], [0xa3e, 0xa42, 0], [0xa47, 0xa48, 0], [0xa4b, 0xa4d, 0], [0xa51, 0xa51, 0], [0xa70, 0xa71, 0],
17
+ [0xa75, 0xa75, 0], [0xa81, 0xa83, 0], [0xabc, 0xabc, 0], [0xabe, 0xac5, 0], [0xac7, 0xac9, 0], [0xacb, 0xacd, 0],
18
+ [0xae2, 0xae3, 0], [0xafa, 0xaff, 0], [0xb01, 0xb03, 0], [0xb3c, 0xb3c, 0], [0xb3e, 0xb44, 0], [0xb47, 0xb48, 0],
19
+ [0xb4b, 0xb4d, 0], [0xb55, 0xb57, 0], [0xb62, 0xb63, 0], [0xb82, 0xb82, 0], [0xbbe, 0xbc2, 0], [0xbc6, 0xbc8, 0],
20
+ [0xbca, 0xbcd, 0], [0xbd7, 0xbd7, 0], [0xc00, 0xc04, 0], [0xc3c, 0xc3c, 0], [0xc3e, 0xc44, 0], [0xc46, 0xc48, 0],
21
+ [0xc4a, 0xc4d, 0], [0xc55, 0xc56, 0], [0xc62, 0xc63, 0], [0xc81, 0xc83, 0], [0xcbc, 0xcbc, 0], [0xcbe, 0xcc4, 0],
22
+ [0xcc6, 0xcc8, 0], [0xcca, 0xccd, 0], [0xcd5, 0xcd6, 0], [0xce2, 0xce3, 0], [0xcf3, 0xcf3, 0], [0xd00, 0xd03, 0],
23
+ [0xd3b, 0xd3c, 0], [0xd3e, 0xd44, 0], [0xd46, 0xd48, 0], [0xd4a, 0xd4d, 0], [0xd57, 0xd57, 0], [0xd62, 0xd63, 0],
24
+ [0xd81, 0xd83, 0], [0xdca, 0xdca, 0], [0xdcf, 0xdd4, 0], [0xdd6, 0xdd6, 0], [0xdd8, 0xddf, 0], [0xdf2, 0xdf3, 0],
25
+ [0xe31, 0xe31, 0], [0xe34, 0xe3a, 0], [0xe47, 0xe4e, 0], [0xeb1, 0xeb1, 0], [0xeb4, 0xebc, 0], [0xec8, 0xece, 0],
26
+ [0xf18, 0xf19, 0], [0xf35, 0xf35, 0], [0xf37, 0xf37, 0], [0xf39, 0xf39, 0], [0xf3e, 0xf3f, 0], [0xf71, 0xf84, 0],
27
+ [0xf86, 0xf87, 0], [0xf8d, 0xf97, 0], [0xf99, 0xfbc, 0], [0xfc6, 0xfc6, 0], [0x102b, 0x103e, 0], [0x1056, 0x1059, 0],
28
+ [0x105e, 0x1060, 0], [0x1062, 0x1064, 0], [0x1067, 0x106d, 0], [0x1071, 0x1074, 0], [0x1082, 0x108d, 0],
29
+ [0x108f, 0x108f, 0], [0x109a, 0x109d, 0], [0x1100, 0x115f, 2], [0x1160, 0x11ff, 0], [0x135d, 0x135f, 0],
30
+ [0x1712, 0x1715, 0], [0x1732, 0x1734, 0], [0x1752, 0x1753, 0], [0x1772, 0x1773, 0], [0x17b4, 0x17d3, 0],
31
+ [0x17dd, 0x17dd, 0], [0x180b, 0x180f, 0], [0x1885, 0x1886, 0], [0x18a9, 0x18a9, 0], [0x1920, 0x192b, 0],
32
+ [0x1930, 0x193b, 0], [0x1a17, 0x1a1b, 0], [0x1a55, 0x1a5e, 0], [0x1a60, 0x1a7c, 0], [0x1a7f, 0x1a7f, 0],
33
+ [0x1ab0, 0x1add, 0], [0x1ae0, 0x1aeb, 0], [0x1b00, 0x1b04, 0], [0x1b34, 0x1b44, 0], [0x1b6b, 0x1b73, 0],
34
+ [0x1b80, 0x1b82, 0], [0x1ba1, 0x1bad, 0], [0x1be6, 0x1bf3, 0], [0x1c24, 0x1c37, 0], [0x1cd0, 0x1cd2, 0],
35
+ [0x1cd4, 0x1ce8, 0], [0x1ced, 0x1ced, 0], [0x1cf4, 0x1cf4, 0], [0x1cf7, 0x1cf9, 0], [0x1dc0, 0x1dff, 0],
36
+ [0x200b, 0x200f, 0], [0x2028, 0x202e, 0], [0x2060, 0x206f, 0], [0x20d0, 0x20f0, 0], [0x231a, 0x231b, 2],
37
+ [0x2329, 0x232a, 2], [0x23e9, 0x23ec, 2], [0x23f0, 0x23f0, 2], [0x23f3, 0x23f3, 2], [0x25fd, 0x25fe, 2],
38
+ [0x2614, 0x2615, 2], [0x2630, 0x2637, 2], [0x2648, 0x2653, 2], [0x267f, 0x267f, 2], [0x268a, 0x268f, 2],
39
+ [0x2693, 0x2693, 2], [0x26a1, 0x26a1, 2], [0x26aa, 0x26ab, 2], [0x26bd, 0x26be, 2], [0x26c4, 0x26c5, 2],
40
+ [0x26ce, 0x26ce, 2], [0x26d4, 0x26d4, 2], [0x26ea, 0x26ea, 2], [0x26f2, 0x26f3, 2], [0x26f5, 0x26f5, 2],
41
+ [0x26fa, 0x26fa, 2], [0x26fd, 0x26fd, 2], [0x2705, 0x2705, 2], [0x270a, 0x270b, 2], [0x2728, 0x2728, 2],
42
+ [0x274c, 0x274c, 2], [0x274e, 0x274e, 2], [0x2753, 0x2755, 2], [0x2757, 0x2757, 2], [0x2795, 0x2797, 2],
43
+ [0x27b0, 0x27b0, 2], [0x27bf, 0x27bf, 2], [0x2b1b, 0x2b1c, 2], [0x2b50, 0x2b50, 2], [0x2b55, 0x2b55, 2],
44
+ [0x2cef, 0x2cf1, 0], [0x2d7f, 0x2d7f, 0], [0x2de0, 0x2dff, 0], [0x2e80, 0x2e99, 2], [0x2e9b, 0x2ef3, 2],
45
+ [0x2f00, 0x2fd5, 2], [0x2ff0, 0x3029, 2], [0x302a, 0x302f, 0], [0x3030, 0x303e, 2], [0x3041, 0x3096, 2],
46
+ [0x3099, 0x309a, 0], [0x309b, 0x30ff, 2], [0x3105, 0x312f, 2], [0x3131, 0x3163, 2], [0x3164, 0x3164, 0],
47
+ [0x3165, 0x318e, 2], [0x3190, 0x31e5, 2], [0x31ef, 0x321e, 2], [0x3220, 0x3247, 2], [0x3250, 0xa48c, 2],
48
+ [0xa490, 0xa4c6, 2], [0xa66f, 0xa672, 0], [0xa674, 0xa67d, 0], [0xa69e, 0xa69f, 0], [0xa6f0, 0xa6f1, 0],
49
+ [0xa802, 0xa802, 0], [0xa806, 0xa806, 0], [0xa80b, 0xa80b, 0], [0xa823, 0xa827, 0], [0xa82c, 0xa82c, 0],
50
+ [0xa880, 0xa881, 0], [0xa8b4, 0xa8c5, 0], [0xa8e0, 0xa8f1, 0], [0xa8ff, 0xa8ff, 0], [0xa926, 0xa92d, 0],
51
+ [0xa947, 0xa953, 0], [0xa960, 0xa97c, 2], [0xa980, 0xa983, 0], [0xa9b3, 0xa9c0, 0], [0xa9e5, 0xa9e5, 0],
52
+ [0xaa29, 0xaa36, 0], [0xaa43, 0xaa43, 0], [0xaa4c, 0xaa4d, 0], [0xaa7b, 0xaa7d, 0], [0xaab0, 0xaab0, 0],
53
+ [0xaab2, 0xaab4, 0], [0xaab7, 0xaab8, 0], [0xaabe, 0xaabf, 0], [0xaac1, 0xaac1, 0], [0xaaeb, 0xaaef, 0],
54
+ [0xaaf5, 0xaaf6, 0], [0xabe3, 0xabea, 0], [0xabec, 0xabed, 0], [0xac00, 0xd7a3, 2], [0xd7b0, 0xd7ff, 0],
55
+ [0xf900, 0xfaff, 2], [0xfb1e, 0xfb1e, 0], [0xfe00, 0xfe0f, 0], [0xfe10, 0xfe19, 2], [0xfe20, 0xfe2f, 0],
56
+ [0xfe30, 0xfe52, 2], [0xfe54, 0xfe66, 2], [0xfe68, 0xfe6b, 2], [0xfeff, 0xfeff, 0], [0xff01, 0xff60, 2],
57
+ [0xffa0, 0xffa0, 0], [0xffe0, 0xffe6, 2], [0xfff0, 0xfffb, 0], [0x101fd, 0x101fd, 0], [0x102e0, 0x102e0, 0],
58
+ [0x10376, 0x1037a, 0], [0x10a01, 0x10a03, 0], [0x10a05, 0x10a06, 0], [0x10a0c, 0x10a0f, 0], [0x10a38, 0x10a3a, 0],
59
+ [0x10a3f, 0x10a3f, 0], [0x10ae5, 0x10ae6, 0], [0x10d24, 0x10d27, 0], [0x10d69, 0x10d6d, 0], [0x10eab, 0x10eac, 0],
60
+ [0x10efa, 0x10eff, 0], [0x10f46, 0x10f50, 0], [0x10f82, 0x10f85, 0], [0x11000, 0x11002, 0], [0x11038, 0x11046, 0],
61
+ [0x11070, 0x11070, 0], [0x11073, 0x11074, 0], [0x1107f, 0x11082, 0], [0x110b0, 0x110ba, 0], [0x110c2, 0x110c2, 0],
62
+ [0x11100, 0x11102, 0], [0x11127, 0x11134, 0], [0x11145, 0x11146, 0], [0x11173, 0x11173, 0], [0x11180, 0x11182, 0],
63
+ [0x111b3, 0x111c0, 0], [0x111c9, 0x111cc, 0], [0x111ce, 0x111cf, 0], [0x1122c, 0x11237, 0], [0x1123e, 0x1123e, 0],
64
+ [0x11241, 0x11241, 0], [0x112df, 0x112ea, 0], [0x11300, 0x11303, 0], [0x1133b, 0x1133c, 0], [0x1133e, 0x11344, 0],
65
+ [0x11347, 0x11348, 0], [0x1134b, 0x1134d, 0], [0x11357, 0x11357, 0], [0x11362, 0x11363, 0], [0x11366, 0x1136c, 0],
66
+ [0x11370, 0x11374, 0], [0x113b8, 0x113c0, 0], [0x113c2, 0x113c2, 0], [0x113c5, 0x113c5, 0], [0x113c7, 0x113ca, 0],
67
+ [0x113cc, 0x113d0, 0], [0x113d2, 0x113d2, 0], [0x113e1, 0x113e2, 0], [0x11435, 0x11446, 0], [0x1145e, 0x1145e, 0],
68
+ [0x114b0, 0x114c3, 0], [0x115af, 0x115b5, 0], [0x115b8, 0x115c0, 0], [0x115dc, 0x115dd, 0], [0x11630, 0x11640, 0],
69
+ [0x116ab, 0x116b7, 0], [0x1171d, 0x1172b, 0], [0x1182c, 0x1183a, 0], [0x11930, 0x11935, 0], [0x11937, 0x11938, 0],
70
+ [0x1193b, 0x1193e, 0], [0x11940, 0x11940, 0], [0x11942, 0x11943, 0], [0x119d1, 0x119d7, 0], [0x119da, 0x119e0, 0],
71
+ [0x119e4, 0x119e4, 0], [0x11a01, 0x11a0a, 0], [0x11a33, 0x11a39, 0], [0x11a3b, 0x11a3e, 0], [0x11a47, 0x11a47, 0],
72
+ [0x11a51, 0x11a5b, 0], [0x11a8a, 0x11a99, 0], [0x11b60, 0x11b67, 0], [0x11c2f, 0x11c36, 0], [0x11c38, 0x11c3f, 0],
73
+ [0x11c92, 0x11ca7, 0], [0x11ca9, 0x11cb6, 0], [0x11d31, 0x11d36, 0], [0x11d3a, 0x11d3a, 0], [0x11d3c, 0x11d3d, 0],
74
+ [0x11d3f, 0x11d45, 0], [0x11d47, 0x11d47, 0], [0x11d8a, 0x11d8e, 0], [0x11d90, 0x11d91, 0], [0x11d93, 0x11d97, 0],
75
+ [0x11ef3, 0x11ef6, 0], [0x11f00, 0x11f01, 0], [0x11f03, 0x11f03, 0], [0x11f34, 0x11f3a, 0], [0x11f3e, 0x11f42, 0],
76
+ [0x11f5a, 0x11f5a, 0], [0x13430, 0x13440, 0], [0x13447, 0x13455, 0], [0x1611e, 0x1612f, 0], [0x16af0, 0x16af4, 0],
77
+ [0x16b30, 0x16b36, 0], [0x16f4f, 0x16f4f, 0], [0x16f51, 0x16f87, 0], [0x16f8f, 0x16f92, 0], [0x16fe0, 0x16fe3, 2],
78
+ [0x16fe4, 0x16fe4, 0], [0x16ff0, 0x16ff1, 0], [0x16ff2, 0x16ff6, 2], [0x17000, 0x18cd5, 2], [0x18cff, 0x18d1e, 2],
79
+ [0x18d80, 0x18df2, 2], [0x1aff0, 0x1aff3, 2], [0x1aff5, 0x1affb, 2], [0x1affd, 0x1affe, 2], [0x1b000, 0x1b122, 2],
80
+ [0x1b132, 0x1b132, 2], [0x1b150, 0x1b152, 2], [0x1b155, 0x1b155, 2], [0x1b164, 0x1b167, 2], [0x1b170, 0x1b2fb, 2],
81
+ [0x1bc9d, 0x1bc9e, 0], [0x1bca0, 0x1bca3, 0], [0x1cf00, 0x1cf2d, 0], [0x1cf30, 0x1cf46, 0], [0x1d165, 0x1d169, 0],
82
+ [0x1d16d, 0x1d182, 0], [0x1d185, 0x1d18b, 0], [0x1d1aa, 0x1d1ad, 0], [0x1d242, 0x1d244, 0], [0x1d300, 0x1d356, 2],
83
+ [0x1d360, 0x1d376, 2], [0x1da00, 0x1da36, 0], [0x1da3b, 0x1da6c, 0], [0x1da75, 0x1da75, 0], [0x1da84, 0x1da84, 0],
84
+ [0x1da9b, 0x1da9f, 0], [0x1daa1, 0x1daaf, 0], [0x1e000, 0x1e006, 0], [0x1e008, 0x1e018, 0], [0x1e01b, 0x1e021, 0],
85
+ [0x1e023, 0x1e024, 0], [0x1e026, 0x1e02a, 0], [0x1e08f, 0x1e08f, 0], [0x1e130, 0x1e136, 0], [0x1e2ae, 0x1e2ae, 0],
86
+ [0x1e2ec, 0x1e2ef, 0], [0x1e4ec, 0x1e4ef, 0], [0x1e5ee, 0x1e5ef, 0], [0x1e6e3, 0x1e6e3, 0], [0x1e6e6, 0x1e6e6, 0],
87
+ [0x1e6ee, 0x1e6ef, 0], [0x1e6f5, 0x1e6f5, 0], [0x1e8d0, 0x1e8d6, 0], [0x1e944, 0x1e94a, 0], [0x1f004, 0x1f004, 2],
88
+ [0x1f0cf, 0x1f0cf, 2], [0x1f18e, 0x1f18e, 2], [0x1f191, 0x1f19a, 2], [0x1f200, 0x1f202, 2], [0x1f210, 0x1f23b, 2],
89
+ [0x1f240, 0x1f248, 2], [0x1f250, 0x1f251, 2], [0x1f260, 0x1f265, 2], [0x1f300, 0x1f320, 2], [0x1f32d, 0x1f335, 2],
90
+ [0x1f337, 0x1f37c, 2], [0x1f37e, 0x1f393, 2], [0x1f3a0, 0x1f3ca, 2], [0x1f3cf, 0x1f3d3, 2], [0x1f3e0, 0x1f3f0, 2],
91
+ [0x1f3f4, 0x1f3f4, 2], [0x1f3f8, 0x1f3fa, 2], [0x1f3fb, 0x1f3ff, 0], [0x1f400, 0x1f43e, 2], [0x1f440, 0x1f440, 2],
92
+ [0x1f442, 0x1f4fc, 2], [0x1f4ff, 0x1f53d, 2], [0x1f54b, 0x1f54e, 2], [0x1f550, 0x1f567, 2], [0x1f57a, 0x1f57a, 2],
93
+ [0x1f595, 0x1f596, 2], [0x1f5a4, 0x1f5a4, 2], [0x1f5fb, 0x1f64f, 2], [0x1f680, 0x1f6c5, 2], [0x1f6cc, 0x1f6cc, 2],
94
+ [0x1f6d0, 0x1f6d2, 2], [0x1f6d5, 0x1f6d8, 2], [0x1f6dc, 0x1f6df, 2], [0x1f6eb, 0x1f6ec, 2], [0x1f6f4, 0x1f6fc, 2],
95
+ [0x1f7e0, 0x1f7eb, 2], [0x1f7f0, 0x1f7f0, 2], [0x1f90c, 0x1f93a, 2], [0x1f93c, 0x1f945, 2], [0x1f947, 0x1f9ff, 2],
96
+ [0x1fa70, 0x1fa7c, 2], [0x1fa80, 0x1fa8a, 2], [0x1fa8e, 0x1fac6, 2], [0x1fac8, 0x1fac8, 2], [0x1facd, 0x1fadc, 2],
97
+ [0x1fadf, 0x1faea, 2], [0x1faef, 0x1faf8, 2], [0x20000, 0x2fffd, 2], [0x30000, 0x3fffd, 2], [0xe0000, 0xe0fff, 0],
98
+ ];
99
+ // The code points one cell wide on their own and two when VARIATION SELECTOR-16
100
+ // asks for emoji presentation.
101
+ const NARROW_TO_WIDE = new Set([
102
+ 0x23, 0x2a, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0xa9, 0xae, 0x203c, 0x2049, 0x2122, 0x2139,
103
+ 0x2194, 0x2195, 0x2196, 0x2197, 0x2198, 0x2199, 0x21a9, 0x21aa, 0x2328, 0x23cf, 0x23ed, 0x23ee, 0x23ef, 0x23f1,
104
+ 0x23f2, 0x23f8, 0x23f9, 0x23fa, 0x24c2, 0x25aa, 0x25ab, 0x25b6, 0x25c0, 0x25fb, 0x25fc, 0x2600, 0x2601, 0x2602,
105
+ 0x2603, 0x2604, 0x260e, 0x2611, 0x2618, 0x261d, 0x2620, 0x2622, 0x2623, 0x2626, 0x262a, 0x262e, 0x262f, 0x2638,
106
+ 0x2639, 0x263a, 0x2640, 0x2642, 0x265f, 0x2660, 0x2663, 0x2665, 0x2666, 0x2668, 0x267b, 0x267e, 0x2692, 0x2694,
107
+ 0x2695, 0x2696, 0x2697, 0x2699, 0x269b, 0x269c, 0x26a0, 0x26a7, 0x26b0, 0x26b1, 0x26c8, 0x26cf, 0x26d1, 0x26d3,
108
+ 0x26e9, 0x26f0, 0x26f1, 0x26f4, 0x26f7, 0x26f8, 0x26f9, 0x2702, 0x2708, 0x2709, 0x270c, 0x270d, 0x270f, 0x2712,
109
+ 0x2714, 0x2716, 0x271d, 0x2721, 0x2733, 0x2734, 0x2744, 0x2747, 0x2763, 0x2764, 0x27a1, 0x2934, 0x2935, 0x2b05,
110
+ 0x2b06, 0x2b07, 0x1f170, 0x1f171, 0x1f17e, 0x1f17f, 0x1f321, 0x1f324, 0x1f325, 0x1f326, 0x1f327, 0x1f328, 0x1f329,
111
+ 0x1f32a, 0x1f32b, 0x1f32c, 0x1f336, 0x1f37d, 0x1f396, 0x1f397, 0x1f399, 0x1f39a, 0x1f39b, 0x1f39e, 0x1f39f, 0x1f3cb,
112
+ 0x1f3cc, 0x1f3cd, 0x1f3ce, 0x1f3d4, 0x1f3d5, 0x1f3d6, 0x1f3d7, 0x1f3d8, 0x1f3d9, 0x1f3da, 0x1f3db, 0x1f3dc, 0x1f3dd,
113
+ 0x1f3de, 0x1f3df, 0x1f3f3, 0x1f3f5, 0x1f3f7, 0x1f43f, 0x1f441, 0x1f4fd, 0x1f549, 0x1f54a, 0x1f56f, 0x1f570, 0x1f573,
114
+ 0x1f574, 0x1f575, 0x1f576, 0x1f577, 0x1f578, 0x1f579, 0x1f587, 0x1f58a, 0x1f58b, 0x1f58c, 0x1f58d, 0x1f590, 0x1f5a5,
115
+ 0x1f5a8, 0x1f5b1, 0x1f5b2, 0x1f5bc, 0x1f5c2, 0x1f5c3, 0x1f5c4, 0x1f5d1, 0x1f5d2, 0x1f5d3, 0x1f5dc, 0x1f5dd, 0x1f5de,
116
+ 0x1f5e1, 0x1f5e3, 0x1f5e8, 0x1f5ef, 0x1f5f3, 0x1f5fa, 0x1f6cb, 0x1f6cd, 0x1f6ce, 0x1f6cf, 0x1f6e0, 0x1f6e1, 0x1f6e2,
117
+ 0x1f6e3, 0x1f6e4, 0x1f6e5, 0x1f6e9, 0x1f6f0, 0x1f6f3,
118
+ ]);
119
+ /** The cells Rich gives `codePoint` on its own: none for a C0 or C1 control. */
120
+ export function codePointWidth(codePoint) {
121
+ if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0))
122
+ return 0;
123
+ let low = 0;
124
+ let high = RANGES.length - 1;
125
+ while (low <= high) {
126
+ const mid = (low + high) >> 1;
127
+ const [first, last, cells] = RANGES[mid];
128
+ if (codePoint < first)
129
+ high = mid - 1;
130
+ else if (codePoint > last)
131
+ low = mid + 1;
132
+ else
133
+ return cells;
134
+ }
135
+ return 1;
136
+ }
137
+ /** Whether VS16 after `codePoint` widens it from one cell to two. */
138
+ export function widensToEmoji(codePoint) {
139
+ return NARROW_TO_WIDE.has(codePoint);
140
+ }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Terminal cell width calculation.
3
- * Handles ASCII, CJK (double-width), and emoji characters.
2
+ * Terminal cell width calculation: Rich's `cell_len`, ported.
4
3
  */
5
4
  /**
6
5
  * Returns the terminal cell width of a string.
@@ -72,13 +71,20 @@ export declare function setCellSize(text: string, totalWidth: CellCol): string;
72
71
  */
73
72
  export declare function splitText(text: string, position: CellCol): [string, string];
74
73
  /**
75
- * Cuts `text` into exactly `position` cells and the rest, as the reference's
76
- * `Segment.split_cells` does: a wide glyph the cut goes through fits neither
77
- * side, so it leaves a space in each of its cells, where `splitText` keeps it
78
- * whole on the right. Every other character lands on exactly one side.
79
- * `position` must fall inside `text`, between 0 and its width.
74
+ * Cuts `text` at each of `positions`, as the reference's `Segment.split_cells`
75
+ * cuts at one: a wide glyph a cut goes through fits neither side, so it leaves
76
+ * a space in each of its cells, where `splitText` keeps it whole on the right.
77
+ * Every other character lands in exactly one piece, so each piece is exactly
78
+ * as wide as the distance between its cuts. `positions` are non-decreasing and
79
+ * fall inside `text`: from 0 to one short of its width.
80
+ *
81
+ * One walk over `text` however many positions there are, ending at the last
82
+ * of them: cutting what is left again at every position re-measures and copies
83
+ * the rest each time, which is quadratic in a long enough string, and walking
84
+ * past the last position makes one cut near the start of a long string pay for
85
+ * all of it.
80
86
  */
81
- export declare function cutCells(text: string, position: CellCol): [string, string];
87
+ export declare function cutCells(text: string, positions: readonly CellCol[]): string[];
82
88
  /**
83
89
  * Wraps text into lines of at most `maxWidth` cells, preserving every code
84
90
  * point: `chopCells(t, w).join("") === t`. Unlike `splitText` this never pads,
@@ -97,9 +103,9 @@ export declare function chopCells(text: string, maxWidth: CellCol): string[];
97
103
  */
98
104
  export declare function splitAtCells(line: string, cuts: readonly CellCol[]): string[];
99
105
  /**
100
- * The grapheme clusters of `text`, in order: the unit `string-width` measures,
101
- * and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
102
- * over code points instead disagrees with it on every glyph built from several:
106
+ * The grapheme clusters of `text`, in order: the unit `cellLen` of a whole
107
+ * string is the sum of. Summing `cellLen` over code points instead disagrees
108
+ * with it on every glyph built from several:
103
109
  * `❤️` is 1 + 0 by code point and 2 as a cluster, `👨‍👩‍👧` is 6 and 2, and a cut
104
110
  * between two of its code points leaves half a glyph.
105
111
  */
@@ -1,11 +1,57 @@
1
1
  /**
2
- * Terminal cell width calculation.
3
- * Handles ASCII, CJK (double-width), and emoji characters.
2
+ * Terminal cell width calculation: Rich's `cell_len`, ported.
4
3
  */
5
- import stringWidth from "string-width";
4
+ import { codePointWidth, widensToEmoji } from "./cell-widths.js";
6
5
  import { Memo } from "./memo.js";
7
- // [LAW:one-source-of-truth] string-width is the single authority for cell width
8
6
  const cellLenMemo = new Memo();
7
+ const ZWJ = 0x200d;
8
+ const VS16 = 0xfe0f;
9
+ const JOINERS = /[\u200d\ufe0f]/;
10
+ const PRINTABLE_ASCII = /^[ -~]*$/;
11
+ // Rich's `cell_len` over a run of text: each code point the cells Rich's table
12
+ // gives it, but that a ZWJ takes the code point after it into the glyph before
13
+ // so neither adds a cell, and VS16 widens the last code point measured to two
14
+ // cells when the table lists it as one that emoji presentation widens.
15
+ function richCells(text) {
16
+ let cells = 0;
17
+ let widenable = false;
18
+ let joined = false;
19
+ for (const ch of text) {
20
+ const codePoint = ch.codePointAt(0);
21
+ if (joined) {
22
+ joined = false;
23
+ }
24
+ else if (codePoint === ZWJ) {
25
+ joined = true;
26
+ }
27
+ else if (codePoint === VS16) {
28
+ cells += widenable ? 1 : 0;
29
+ widenable = false;
30
+ }
31
+ else {
32
+ const width = codePointWidth(codePoint);
33
+ cells += width;
34
+ widenable = width > 0 ? widensToEmoji(codePoint) : widenable;
35
+ }
36
+ }
37
+ return cells;
38
+ }
39
+ // [LAW:one-source-of-truth] the single authority for cell width. A string is
40
+ // the sum of its grapheme clusters, each measured as Rich measures it, so a cut
41
+ // between clusters splits the width exactly; the two part only where a ZWJ or
42
+ // VS16 reaches across a cluster boundary, as in `a` ZWJ `b`, which Rich counts
43
+ // as one cell and two clusters count as two. Without either, every code point
44
+ // counts alone and no segmentation is needed.
45
+ function measure(text) {
46
+ if (PRINTABLE_ASCII.test(text))
47
+ return text.length;
48
+ if (!JOINERS.test(text))
49
+ return richCells(text);
50
+ let cells = 0;
51
+ for (const { segment } of graphemeSegmenter().segment(text))
52
+ cells += richCells(segment);
53
+ return cells;
54
+ }
9
55
  /**
10
56
  * Returns the terminal cell width of a string.
11
57
  */
@@ -13,7 +59,7 @@ export function cellLen(text) {
13
59
  if (text.length === 0)
14
60
  return 0;
15
61
  // Short strings recur (words, cells, labels); a long one is rarely measured twice.
16
- return text.length <= 64 ? cellLenMemo.get(text, stringWidth) : stringWidth(text);
62
+ return text.length <= 64 ? cellLenMemo.get(text, measure) : measure(text);
17
63
  }
18
64
  /** Brand a raw number as a CellCol. Use only at trust boundaries. */
19
65
  export function asCellCol(n) { return n; }
@@ -82,22 +128,49 @@ export function splitText(text, position) {
82
128
  return [left + " ".repeat(position - cellLen(left)), text.slice(left.length)];
83
129
  }
84
130
  /**
85
- * Cuts `text` into exactly `position` cells and the rest, as the reference's
86
- * `Segment.split_cells` does: a wide glyph the cut goes through fits neither
87
- * side, so it leaves a space in each of its cells, where `splitText` keeps it
88
- * whole on the right. Every other character lands on exactly one side.
89
- * `position` must fall inside `text`, between 0 and its width.
131
+ * Cuts `text` at each of `positions`, as the reference's `Segment.split_cells`
132
+ * cuts at one: a wide glyph a cut goes through fits neither side, so it leaves
133
+ * a space in each of its cells, where `splitText` keeps it whole on the right.
134
+ * Every other character lands in exactly one piece, so each piece is exactly
135
+ * as wide as the distance between its cuts. `positions` are non-decreasing and
136
+ * fall inside `text`: from 0 to one short of its width.
137
+ *
138
+ * One walk over `text` however many positions there are, ending at the last
139
+ * of them: cutting what is left again at every position re-measures and copies
140
+ * the rest each time, which is quadratic in a long enough string, and walking
141
+ * past the last position makes one cut near the start of a long string pay for
142
+ * all of it.
90
143
  */
91
- export function cutCells(text, position) {
92
- const head = cellFit(text, position);
93
- // Cells of the glyph the cut goes through that lie left of it; 0 when the
94
- // cut falls between glyphs, and then there is no such glyph.
95
- const straddle = position - cellLen(head);
96
- const [glyph = ""] = straddle > 0 ? clustersFrom(text, asCodePoint(head.length)) : [];
97
- return [
98
- head + " ".repeat(straddle),
99
- " ".repeat(cellLen(glyph) - straddle) + text.slice(head.length + glyph.length),
100
- ];
144
+ export function cutCells(text, positions) {
145
+ const pieces = [];
146
+ // The current piece is `lead` blank cells, then `text` from `start` to `at`.
147
+ let start = 0;
148
+ let lead = 0;
149
+ let at = 0;
150
+ let col = 0;
151
+ let next = 0;
152
+ for (const glyph of clustersFrom(text, asCodePoint(0))) {
153
+ if (next === positions.length)
154
+ break;
155
+ const end = col + cellLen(glyph);
156
+ // The first cell of this glyph no piece has taken yet.
157
+ let from = col;
158
+ for (; next < positions.length && positions[next] < end; next++) {
159
+ const cut = positions[next];
160
+ pieces.push(" ".repeat(lead) + text.slice(start, at) + " ".repeat(cut - from));
161
+ lead = 0;
162
+ // A cut between glyphs leaves this one to the next piece; a cut through
163
+ // it blanks it, and the pieces after the cut start past it.
164
+ start = cut === col ? at : at + glyph.length;
165
+ from = cut;
166
+ }
167
+ if (from > col)
168
+ lead = end - from;
169
+ at += glyph.length;
170
+ col = end;
171
+ }
172
+ pieces.push(" ".repeat(lead) + text.slice(start));
173
+ return pieces;
101
174
  }
102
175
  /**
103
176
  * Wraps text into lines of at most `maxWidth` cells, preserving every code
@@ -146,18 +219,20 @@ export function splitAtCells(line, cuts) {
146
219
  // stepping along one string does not re-segment the whole of it every step.
147
220
  let segmenter;
148
221
  let lastSegmented;
222
+ function graphemeSegmenter() {
223
+ return (segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }));
224
+ }
149
225
  /** The grapheme segmentation of `text`, reusing the last one when it is the same string. */
150
226
  function segmentsOf(text) {
151
227
  if (lastSegmented?.text !== text) {
152
- segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
153
- lastSegmented = { text, segments: segmenter.segment(text) };
228
+ lastSegmented = { text, segments: graphemeSegmenter().segment(text) };
154
229
  }
155
230
  return lastSegmented.segments;
156
231
  }
157
232
  /**
158
- * The grapheme clusters of `text`, in order: the unit `string-width` measures,
159
- * and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
160
- * over code points instead disagrees with it on every glyph built from several:
233
+ * The grapheme clusters of `text`, in order: the unit `cellLen` of a whole
234
+ * string is the sum of. Summing `cellLen` over code points instead disagrees
235
+ * with it on every glyph built from several:
161
236
  * `❤️` is 1 + 0 by code point and 2 as a cluster, `👨‍👩‍👧` is 6 and 2, and a cut
162
237
  * between two of its code points leaves half a glyph.
163
238
  */
@@ -18,7 +18,7 @@ import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
18
18
  import { Rule } from "../renderables/rule.js";
19
19
  import { segmentsToString } from "./render.js";
20
20
  import { placeBlock } from "./place.js";
21
- import { isRenderable } from "./protocol.js";
21
+ import { isRenderable, render } from "./protocol.js";
22
22
  // [LAW:dataflow-not-control-flow] With no live region claimed, stepping around
23
23
  // it is the text itself: every write takes the same path.
24
24
  const NO_LIVE_REGION = { around: (text) => text };
@@ -67,7 +67,7 @@ class TextRun {
67
67
  this.text = items.reduce((joined, item) => joined.append(item), new RichText("", { end: "" }));
68
68
  }
69
69
  *render(options) {
70
- yield* this.text.render(options);
70
+ yield* render(this.text, options);
71
71
  yield this.end;
72
72
  }
73
73
  measure(options) {
@@ -433,7 +433,7 @@ export class Console {
433
433
  const renderable = block.kind === "text" ? new TextRun(block.items, terminator) : block.renderable;
434
434
  if (align)
435
435
  return { lines: placeBlock(renderable, align, renderOpts), closed: true };
436
- const drawn = [...renderable.render(renderOpts)];
436
+ const drawn = [...render(renderable, renderOpts)];
437
437
  // A text run's last segment is always its `end`, yielded even when
438
438
  // empty, and `splitLines` drops the empty line after a final break only
439
439
  // when nothing follows it — so the run is closed exactly when `end` is.
@@ -479,7 +479,7 @@ export class Console {
479
479
  time.stylize("log.time");
480
480
  const width = Math.min(time.cellLength, options.maxWidth - 1);
481
481
  const { rows, cropWidth } = this._draw(args, { ...options, maxWidth: options.maxWidth - width });
482
- const [stampLine = []] = Segment.splitLines(time.render({ ...options, maxWidth: time.cellLength }));
482
+ const [stampLine = []] = Segment.splitLines(render(time, { ...options, maxWidth: time.cellLength }));
483
483
  const styled = (cell) => [...Segment.applyStyle(cell, this._style)];
484
484
  const stamp = styled(Segment.adjustLineLength(stampLine, width));
485
485
  const blank = styled([new Segment(" ".repeat(width))]);
@@ -499,7 +499,7 @@ export class Console {
499
499
  // into this constructor, which made a second map of one shape — identical
500
500
  // then, free to drift the moment `Rule` grew an option.
501
501
  rule(title, options) {
502
- const segments = [...new Rule(title, options).render(this.options)];
502
+ const segments = [...render(new Rule(title, options), this.options)];
503
503
  this._writeSegments(segments);
504
504
  }
505
505
  // [LAW:one-source-of-truth] `JSONRenderable` is the one formatter for JSON and