@promptctl/rich-js 0.24.1 → 0.25.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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +24 -33
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +98 -29
  47. package/dist/core/strip.js +211 -150
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +158 -63
  73. package/dist/renderables/effect.js +378 -155
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -29,32 +29,40 @@ import { Console } from "../core/console.js";
29
29
  import { Segment } from "../core/segment.js";
30
30
  import { FinalFramePainter, SurfacePainter } from "../core/paint.js";
31
31
  import { fitHeight, render } from "../core/protocol.js";
32
- // [LAW:parse-dont-validate] The one shape Live holds: a renderable becomes
33
- // the view that answers it at every t, where it is given, so no frame asks
34
- // which it was.
35
- const viewOf = (view) => view === undefined || typeof view === "function" ? view : () => view;
32
+ import { Timeline } from "../core/timeline.js";
33
+ import { drawView, frameTime, hostView, undrawn } from "../core/view.js";
34
+ import { State } from "../core/state.js";
35
+ // Nothing: what a Live made with no view draws until `update` gives it one.
36
+ const NOTHING = { render: () => [] };
36
37
  export class Live {
37
38
  _view;
38
39
  _console;
39
40
  _rate;
40
41
  _clock;
42
+ // The clock moment this host began: what its default timeline plays from,
43
+ // and what a renderable view is drawn for the time since (`HostTime`).
44
+ _epoch;
45
+ _timeline;
41
46
  _autoRefresh;
42
47
  _transient;
43
48
  _verticalOverflow;
44
49
  _painter;
45
- // The one wait for the next auto-refresh frame, armed after each frame.
46
- _waking;
50
+ // The one wait for the next auto-refresh frame, armed after each frame, and
51
+ // the clock moment it is due at.
52
+ _waking = new State(undefined);
47
53
  // [LAW:one-source-of-truth] The console's live region, held from `start()`
48
54
  // to the hand-back — which is what "started" means, so there is no separate
49
55
  // flag to disagree with it.
50
- _held;
56
+ _held = new State(null);
51
57
  constructor(view, options) {
52
- this._view = viewOf(view);
58
+ this._view = new State(hostView(view ?? NOTHING));
53
59
  this._console = options?.console ?? new Console();
54
60
  // [LAW:parse-dont-validate] Parsed whether or not it auto-refreshes, so a
55
61
  // rate that could never tick is refused where it is given.
56
62
  this._rate = frameRate(options?.refreshPerSecond ?? 4);
57
63
  this._clock = options?.clock ?? systemClock();
64
+ this._epoch = this._clock.now();
65
+ this._timeline = new State(options?.timeline ?? Timeline.play(this._epoch));
58
66
  this._autoRefresh = options?.autoRefresh !== false;
59
67
  this._transient = options?.transient ?? false;
60
68
  this._verticalOverflow = options?.verticalOverflow ?? "ellipsis";
@@ -65,7 +73,6 @@ export class Live {
65
73
  this._painter = this._console.isInteractive
66
74
  ? new SurfacePainter(options?.altScreen ? "alternate" : "inline", (bytes) => void this._console.file.write(bytes))
67
75
  : new FinalFramePainter((frame) => this._console.print(linesOf(frame)));
68
- this._held = null;
69
76
  }
70
77
  /**
71
78
  * The console to print through while this Live runs: what it prints lands
@@ -74,20 +81,45 @@ export class Live {
74
81
  get console() {
75
82
  return this._console;
76
83
  }
84
+ /** The clock the auto-refresh waits on: where the host time a `Timeline` operation takes is read. */
85
+ get clock() {
86
+ return this._clock;
87
+ }
88
+ /** The map from the clock to the time the view is drawn for. */
89
+ get timeline() {
90
+ return this._timeline.get();
91
+ }
92
+ /** The time a frame drawn now is drawn for: the clock's now, through the timeline. */
93
+ get frameTime() {
94
+ return frameTime(this._time());
95
+ }
96
+ _time() {
97
+ return { now: this._clock.now(), epoch: this._epoch, timeline: this._timeline.get() };
98
+ }
99
+ /**
100
+ * Replace the timeline and draw the frame for the time it now maps the
101
+ * clock's now to, so a seek shows where it sought, once. Paused, the frame
102
+ * then holds — the auto-refresh waits on nothing until the timeline plays
103
+ * again — while `refresh()` and `update(…, { refresh: true })` still draw.
104
+ */
105
+ set timeline(timeline) {
106
+ this._timeline.set(timeline);
107
+ this.refresh();
108
+ }
77
109
  start() {
78
- if (this._held !== null)
110
+ if (this._held.get() !== null)
79
111
  return;
80
112
  // Claimed before a byte is written, so a console another Live is running
81
113
  // on refuses this one with the terminal untouched.
82
114
  const held = this._console.claimLiveRegion({ around: (text) => this._stepAround(text) });
83
- this._held = held;
115
+ this._held.set(held);
84
116
  this._painter.take();
85
117
  // Nothing is drawn yet, so nothing has said when it changes: the first
86
118
  // auto-refresh frame comes at the rate, from now.
87
- this._wake(held, { t: this._clock.now(), nextChange: Number.POSITIVE_INFINITY });
119
+ this._wake(held, undrawn(this._clock.now()));
88
120
  }
89
121
  stop() {
90
- const held = this._held;
122
+ const held = this._held.get();
91
123
  if (held === null)
92
124
  return;
93
125
  // [LAW:no-silent-failure] A final frame that throws still propagates, and
@@ -106,17 +138,29 @@ export class Live {
106
138
  }
107
139
  update(view, options) {
108
140
  if (view !== undefined) {
109
- this._view = viewOf(view);
141
+ this._view.set(hostView(view));
110
142
  }
111
143
  if (options?.refresh) {
112
144
  this.refresh();
145
+ return;
146
+ }
147
+ const held = this._held.get();
148
+ if (view !== undefined && held !== null) {
149
+ // The last wake was armed for what the old view said; a new view has
150
+ // said nothing yet, so the auto-refresh draws it by the rate's next
151
+ // frame from now — sooner if the wake already armed is, and never
152
+ // later, so updates faster than the rate do not put the frame off.
153
+ const now = this._clock.now();
154
+ const waking = this._waking.get();
155
+ if (waking === undefined || waking.at > now + this._rate.interval)
156
+ this._wake(held, undrawn(now));
113
157
  }
114
158
  }
115
159
  refresh() {
116
160
  // A frame is drawn only while this Live holds the terminal, as in Rich:
117
161
  // before `start()` or after the hand-back it would land on a screen that
118
162
  // belongs to someone else — the main buffer, under the user's own output.
119
- const held = this._held;
163
+ const held = this._held.get();
120
164
  if (held === null)
121
165
  return;
122
166
  // [LAW:no-ambient-temporal-coupling] The frame says when what it shows
@@ -126,30 +170,33 @@ export class Live {
126
170
  // frame not drawn has said nothing.
127
171
  let said;
128
172
  this._paint(() => {
129
- const { t, lines } = this._frame();
130
- said = { t, nextChange: lines.reduce((soonest, row) => Math.min(soonest, Segment.nextChange(row)), Number.POSITIVE_INFINITY) };
131
- return lines;
173
+ const drawn = this._frame();
174
+ said = drawn;
175
+ return drawn.lines;
132
176
  });
133
- this._wake(held, said ?? { t: this._clock.now(), nextChange: Number.POSITIVE_INFINITY });
134
- }
135
- // Arm the one wait for the next auto-refresh frame while `held`: at the
136
- // frame's `nextChange` when that is before the rate's next frame, else at
137
- // the rate — both measured from the frame's own `t`, never from a second
138
- // reading of the clock, so the rate does not stretch by what a frame cost
139
- // to draw, and a change that fell due while it was drawn is drawn at once
140
- // (`Clock.at` fires now for a time already past). Content that changes
141
- // every frame answers `t` itself, and is drawn at the rate.
177
+ this._wake(held, said ?? undrawn(this._clock.now()));
178
+ }
179
+ // Arm the one wait for the next auto-refresh frame while `held`, from what
180
+ // the frame says: at its `nextChange` when it holds until a moment sooner
181
+ // than the rate, and at the rate otherwise — every view is drawn from state
182
+ // that may change without saying so, which the auto-refresh polls, as
183
+ // Rich's does, a paused timeline's included. Every moment is measured from the
184
+ // frame's own `t`, never from a second reading of the clock, so the rate
185
+ // does not stretch by what a frame cost to draw, and a change that fell
186
+ // due while it was drawn is drawn at once (`Clock.at` fires now for a time
187
+ // already past).
142
188
  _wake(held, { t, nextChange }) {
143
- this._waking?.();
144
- this._waking = undefined;
189
+ this._waking.get()?.stop();
190
+ this._waking.set(undefined);
191
+ const rate = t + this._rate.interval;
192
+ const at = nextChange <= t ? rate : Math.min(nextChange, rate);
145
193
  if (!this._autoRefresh)
146
194
  return;
147
- const at = Math.min(t + this._rate.interval, nextChange > t ? nextChange : Number.POSITIVE_INFINITY);
148
195
  // No caller is on the stack to run `stop()` for a frame that throws
149
196
  // here, and in Node the throw ends the process — so the terminal is
150
197
  // handed back first, and the error then goes on to wherever an uncaught
151
198
  // one goes.
152
- this._waking = this._clock.at(at, () => {
199
+ const stop = this._clock.at(at, () => {
153
200
  try {
154
201
  this.refresh();
155
202
  }
@@ -158,15 +205,13 @@ export class Live {
158
205
  throw error;
159
206
  }
160
207
  });
208
+ this._waking.set({ at, stop });
161
209
  }
162
- // The frame at the clock's now, with that time: the one the view was handed, and the one the next wake is measured from.
210
+ // The frame at the clock's now, drawn for the time the timeline maps it to,
211
+ // with when it next changes on the clock: what the next wake is measured from.
163
212
  _frame() {
164
213
  // [LAW:effects-at-boundaries] One read of the clock a frame, handed to
165
214
  // the view as the frame's time.
166
- const t = this._clock.now();
167
- if (!this._view)
168
- return { t, lines: [] };
169
- const renderable = this._view(t);
170
215
  // [LAW:dataflow-not-control-flow] Both modes render under the terminal's
171
216
  // rows and differ only in what those rows are: the alternate screen is a
172
217
  // region the frame stands in, and an inline frame keeps its natural height
@@ -174,8 +219,10 @@ export class Live {
174
219
  // back — its overflow policy, then `fitHeight`, which pads only a region.
175
220
  const options = this._console.options;
176
221
  const height = this._painter.height(options.height.rows);
177
- const lines = Segment.splitLines(render(renderable, { ...options, height }));
178
- return { t, lines: fitHeight(this._overflow(lines, height.rows), height) };
222
+ return drawView(this._view.get(), this._time(), this, { ...options, height }, (renderable, drawn) => {
223
+ const lines = Segment.splitLines(render(renderable, drawn));
224
+ return fitHeight(this._overflow(lines, height.rows), height);
225
+ });
179
226
  }
180
227
  // What the console writes goes where the frame was, and the frame is painted
181
228
  // again under it, as Rich's render hook does — in the bytes the console
@@ -210,9 +257,9 @@ export class Live {
210
257
  // ran or a frame threw with no caller to run it.
211
258
  _release(held) {
212
259
  held();
213
- this._held = null;
214
- this._waking?.();
215
- this._waking = undefined;
260
+ this._held.set(null);
261
+ this._waking.get()?.stop();
262
+ this._waking.set(undefined);
216
263
  this._painter.handBack();
217
264
  }
218
265
  }
@@ -25,7 +25,7 @@ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
25
25
  * `parseMarkdown` hands back. `Mark` is what the block pass keeps besides:
26
26
  * none, once the inline pass has dropped its marks.
27
27
  */
28
- export type MdToken<Text = readonly Inline[], Mark = never> = {
28
+ export type MdToken<Text = readonly Inline[], Mark = never> = Readonly<{
29
29
  type: "heading";
30
30
  level: HeadingLevel;
31
31
  text: Text;
@@ -56,7 +56,7 @@ export type MdToken<Text = readonly Inline[], Mark = never> = {
56
56
  /** Kept, though nothing draws it, as Rich exits it like any other block. */
57
57
  | {
58
58
  type: "html_block";
59
- } | Mark;
59
+ } | Mark>;
60
60
  /** The element markdown-it makes of a pair of delimiters. */
61
61
  type EmphasisTag = "em" | "strong" | "s";
62
62
  /**
@@ -66,7 +66,7 @@ type EmphasisTag = "em" | "strong" | "s";
66
66
  * characters they stand for. Emphasis is named by the element markdown-it
67
67
  * makes of it, GFM's strikethrough `s` among them.
68
68
  */
69
- export type Inline = {
69
+ export type Inline = Readonly<{
70
70
  kind: "text";
71
71
  text: string;
72
72
  } | {
@@ -103,7 +103,7 @@ export type Inline = {
103
103
  kind: "softbreak";
104
104
  } | {
105
105
  kind: "hardbreak";
106
- };
106
+ }>;
107
107
  /** `source` read as Markdown: its blocks, and the text in each read into inlines. A line ends at `\r\n`, `\r` or `\n`, as markdown-it normalizes them. */
108
108
  export declare function parseMarkdown(source: string): MdToken[];
109
109
  export {};
@@ -14,6 +14,7 @@
14
14
  import { linkHref, normalizeLinkText } from "./markdown-url.js";
15
15
  import { PY_WHITESPACE, pyStrip } from "./python-space.js";
16
16
  import { namedReference } from "./html-entities.js";
17
+ import { pattern } from "../core/pattern.js";
17
18
  /** A line of the source as the top level holds it. */
18
19
  function sourceLine(text) {
19
20
  return { text, marked: 0, quoteStart: 0, margin: 0, lead: advance(0, LEAD.exec(text)[0]), lazy: false };
@@ -54,35 +55,35 @@ class Lines {
54
55
  }
55
56
  }
56
57
  /** An ATX heading's opening: one to six `#`, then a space, a tab or the end of the line. */
57
- const HEADING = /^#{1,6}(?=[ \t]|$)/;
58
+ const HEADING = pattern(/^#{1,6}(?=[ \t]|$)/);
58
59
  /**
59
60
  * An ATX heading's closing sequence, as markdown-it cuts one: a run of `#` at
60
61
  * the end of the line, spaces and tabs after it allowed, with a space or tab
61
62
  * before it — so `# x#` keeps its `#` and `# #` is an empty heading.
62
63
  */
63
- const HEADING_CLOSE = /(?<=[ \t])#+[ \t]*$/;
64
+ const HEADING_CLOSE = pattern(/(?<=[ \t])#+[ \t]*$/);
64
65
  /** A thematic break: three or more of one of `*`, `-` or `_`, spaces and tabs between them allowed. */
65
- const RULE = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
66
- const SETEXT = /^(=+|-+)[ \t]*$/;
66
+ const RULE = pattern(/^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/);
67
+ const SETEXT = pattern(/^(=+|-+)[ \t]*$/);
67
68
  /**
68
69
  * A code fence's opening, as markdown-it's fence rule reads one: three or more
69
70
  * backticks, the info string after them holding no backtick, or three or more
70
71
  * tildes, the info string free to hold anything. The run is captured whole, so
71
72
  * its closing can be read off it.
72
73
  */
73
- const FENCE = /^(`{3,}(?=[^`]*$)|~{3,})/;
74
- const QUOTE = /^>/;
75
- const ITEM_MARKER = /^(?:[*+-]|(\d{1,9})[.)])/;
74
+ const FENCE = pattern(/^(`{3,}(?=[^`]*$)|~{3,})/);
75
+ const QUOTE = pattern(/^>/);
76
+ const ITEM_MARKER = pattern(/^(?:[*+-]|(\d{1,9})[.)])/);
76
77
  /**
77
78
  * A line's leading space and tab: CommonMark's whitespace, and the only
78
79
  * whitespace markdown-it's block pass reads — what indents a line, what makes
79
80
  * one blank. A no-break space or any other Unicode space is text to it.
80
81
  */
81
- const LEAD = /^[ \t]*/;
82
+ const LEAD = pattern(/^[ \t]*/);
82
83
  const BLOCK_STARTS = [HEADING, FENCE, QUOTE, RULE];
83
84
  /** A line that may be a table's delimiter row, screened as markdown-it screens one: pipes, dashes, colons and space only, and not a dash then a space, which opens a list item. */
84
- const DELIMITER_ROW = /^(?!-[ \t])[-:|][-:| \t]+$/;
85
- const DELIMITER_CELL = /^:?-+:?$/;
85
+ const DELIMITER_ROW = pattern(/^(?!-[ \t])[-:|][-:| \t]+$/);
86
+ const DELIMITER_CELL = pattern(/^:?-+:?$/);
86
87
  /**
87
88
  * How many empty cells a table's short rows may be filled out with before the
88
89
  * table ends, as markdown-it caps them: a header of many columns over many
@@ -110,15 +111,15 @@ const HTML_ATTRIBUTE = String.raw `(?:${PY_WHITESPACE}+[a-zA-Z_:][a-zA-Z0-9:._-]
110
111
  const HTML_OPEN_TAG = String.raw `<[A-Za-z][A-Za-z0-9-]*${HTML_ATTRIBUTE}*${PY_WHITESPACE}*\/?>`;
111
112
  const HTML_CLOSE_TAG = String.raw `<\/[A-Za-z][A-Za-z0-9-]*${PY_WHITESPACE}*>`;
112
113
  const HTML_BLOCKS = [
113
- { start: new RegExp(String.raw `^<(?:script|pre|style|textarea)(?=${PY_WHITESPACE}|>|$)`, "i"), end: /<\/(?:script|pre|style|textarea)>/i, interrupts: true },
114
- { start: /^<!--/, end: /-->/, interrupts: true },
115
- { start: /^<\?/, end: /\?>/, interrupts: true },
114
+ { start: pattern(new RegExp(String.raw `^<(?:script|pre|style|textarea)(?=${PY_WHITESPACE}|>|$)`, "i")), end: pattern(/<\/(?:script|pre|style|textarea)>/i), interrupts: true },
115
+ { start: pattern(/^<!--/), end: pattern(/-->/), interrupts: true },
116
+ { start: pattern(/^<\?/), end: pattern(/\?>/), interrupts: true },
116
117
  // Uppercase only, as markdown-it-py has it, where markdown-it in JS and CommonMark 0.31 take `<!doctype` too.
117
- { start: /^<![A-Z]/, end: />/, interrupts: true },
118
- { start: /^<!\[CDATA\[/, end: /\]\]>/, interrupts: true },
119
- { start: new RegExp(String.raw `^<\/?(?:${HTML_BLOCK_NAMES})(?=${PY_WHITESPACE}|\/?>|$)`, "i"), end: "blank", interrupts: true },
118
+ { start: pattern(/^<![A-Z]/), end: pattern(/>/), interrupts: true },
119
+ { start: pattern(/^<!\[CDATA\[/), end: pattern(/\]\]>/), interrupts: true },
120
+ { start: pattern(new RegExp(String.raw `^<\/?(?:${HTML_BLOCK_NAMES})(?=${PY_WHITESPACE}|\/?>|$)`, "i")), end: "blank", interrupts: true },
120
121
  {
121
- start: new RegExp(String.raw `^(?:${HTML_OPEN_TAG}|${HTML_CLOSE_TAG})${PY_WHITESPACE}*$`),
122
+ start: pattern(new RegExp(String.raw `^(?:${HTML_OPEN_TAG}|${HTML_CLOSE_TAG})${PY_WHITESPACE}*$`)),
122
123
  end: "blank",
123
124
  interrupts: false,
124
125
  },
@@ -337,9 +338,8 @@ function continuation(lines, start) {
337
338
  * `lower().upper()` folds it.
338
339
  */
339
340
  function normalizeReference(label) {
340
- return pyStrip(label).replace(WHITESPACE_RUN, " ").toLowerCase().toUpperCase();
341
+ return pyStrip(label).replace(new RegExp(`${PY_WHITESPACE}+`, "g"), " ").toLowerCase().toUpperCase();
341
342
  }
342
- const WHITESPACE_RUN = new RegExp(`${PY_WHITESPACE}+`, "g");
343
343
  /**
344
344
  * The link reference definition opening at `start`, if one does, as
345
345
  * markdown-it's reference rule reads it: `[label]:`, space, a destination, and
@@ -546,7 +546,7 @@ function listItem(item, level, lines, start) {
546
546
  * marker at least as long, then nothing but spaces and tabs.
547
547
  */
548
548
  function closingFence(open) {
549
- return new RegExp(`^${open}${open[0]}*[ \\t]*$`);
549
+ return pattern(new RegExp(`^${open}${open[0]}*[ \\t]*$`));
550
550
  }
551
551
  /**
552
552
  * Whether a list item holding `children` swallows the rest of the lines its
@@ -717,54 +717,60 @@ function tokenize(lines, level) {
717
717
  return { tokens, end: i };
718
718
  }
719
719
  /** markdown-it's `isWhiteSpace`: Unicode's space separators, and tab, line feed, vertical tab, form feed and carriage return. */
720
- const WHITESPACE = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/;
720
+ const WHITESPACE = pattern(/^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/);
721
721
  /** markdown-it's `isPunctChar`, which takes in all of ASCII's punctuation: Unicode's punctuation and symbols. */
722
- const PUNCTUATION = /^[\p{P}\p{S}]$/u;
723
- const TICKS = /`+/y;
724
- const TICK_RUNS = /`+/g;
725
- const LINE_INDENT = /[ \t]*/y;
722
+ const PUNCTUATION = pattern(/^[\p{P}\p{S}]$/u);
723
+ function anchored(p) {
724
+ // [LAW:no-silent-failure] Under `m`, `^` also matches after a line break, and the match would no longer be at the index.
725
+ if (p.flags.includes("m"))
726
+ throw new TypeError(`/${p.source}/${p.flags}: a multiline pattern is not anchored to where it is read`);
727
+ return pattern(new RegExp(`^(?:${p.source})`, p.flags));
728
+ }
729
+ const TICKS = pattern(/`+/);
730
+ const TICKS_AT = anchored(TICKS);
731
+ const LINE_INDENT = anchored(pattern(/[ \t]*/));
726
732
  /** markdown-it's autolink: `<`, then text holding no `<` up to the first `>`. */
727
- const AUTOLINK = /<([^<>]*)>/y;
733
+ const AUTOLINK = anchored(pattern(/<([^<>]*)>/));
728
734
  /** What an autolink's text must be: a URI, scheme first, or an email address. */
729
- const AUTOLINK_URI = /^[a-zA-Z][a-zA-Z0-9+.\-]{1,31}:[^<>\x00-\x20]*$/;
730
- const AUTOLINK_EMAIL = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
735
+ const AUTOLINK_URI = pattern(/^[a-zA-Z][a-zA-Z0-9+.\-]{1,31}:[^<>\x00-\x20]*$/);
736
+ const AUTOLINK_EMAIL = pattern(/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/);
731
737
  /** Space as markdown-it's link rule skips it around a destination and title: a space, tab or line ending, not Unicode's. */
732
- const LINK_SPACE = /[ \t\n]*/y;
738
+ const LINK_SPACE = anchored(pattern(/[ \t\n]*/));
733
739
  /**
734
740
  * A link title, as markdown-it's `parseLinkTitle` reads one: in `"`, `'` or
735
741
  * `(…)`, a backslash taking the character after it, and a `(` refusing a
736
742
  * `(…)` title.
737
743
  */
738
- const LINK_TITLE = /"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\((?:\\[\s\S]|[^()\\])*\)/y;
744
+ const LINK_TITLE = anchored(pattern(/"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\((?:\\[\s\S]|[^()\\])*\)/));
739
745
  /** How many parentheses markdown-it's `parseLinkDestination` lets a bare destination hold open: one more and it reads none. */
740
746
  const MAX_DESTINATION_NESTING = 32;
741
747
  /** A backslash escape, as markdown-it reads one: the ASCII punctuation character after it, standing for itself. */
742
- const ESCAPE = /\\([!-\/:-@[-`{-~])/y;
748
+ const ESCAPE = pattern(/\\([!-\/:-@[-`{-~])/);
749
+ const ESCAPE_AT = anchored(ESCAPE);
743
750
  /** A numeric character reference, as markdown-it reads one in text: up to seven decimal digits, or up to six hex. */
744
- const NUMERIC_REFERENCE = /&#([xX][0-9a-fA-F]{1,6}|[0-9]{1,7});/y;
751
+ const NUMERIC_REFERENCE = anchored(pattern(/&#([xX][0-9a-fA-F]{1,6}|[0-9]{1,7});/));
745
752
  /** A named character reference, as markdown-it reads one in text: a letter, then one to 31 letters or digits. */
746
- const NAMED_REFERENCE = /&([A-Za-z][A-Za-z0-9]{1,31});/y;
753
+ const NAMED_REFERENCE = anchored(pattern(/&([A-Za-z][A-Za-z0-9]{1,31});/));
747
754
  /**
748
755
  * A backslash escape or a character reference, as markdown-it's `unescapeAll`
749
756
  * reads them in a link's destination: the reference read more widely than in
750
757
  * text, a name or a `#` then one to 31 letters or digits.
751
758
  */
752
- const UNESCAPE_ALL = new RegExp(`${ESCAPE.source}|&[A-Za-z#][A-Za-z0-9]{1,31};`, "g");
759
+ const unescapes = () => new RegExp(`${ESCAPE.source}|&[A-Za-z#][A-Za-z0-9]{1,31};`, "g");
753
760
  /** A numeric reference as `unescapeAll` reads one: up to eight digits of either base. */
754
- const DESTINATION_NUMERIC = /^&#([xX][0-9a-fA-F]{1,8}|[0-9]{1,8});$/;
761
+ const DESTINATION_NUMERIC = pattern(/^&#([xX][0-9a-fA-F]{1,8}|[0-9]{1,8});$/);
755
762
  /** Inline HTML, as markdown-it-py's HTML_TAG_RE reads it: a tag, a comment, a processing instruction, a declaration or a CDATA section. */
756
- const HTML_TAG = new RegExp([
763
+ const HTML_TAG = anchored(pattern(new RegExp([
757
764
  HTML_OPEN_TAG,
758
765
  HTML_CLOSE_TAG,
759
766
  String.raw `<!---?>|<!--(?:[^-]|-[^-]|--[^>])*-->`,
760
767
  String.raw `<[?][\s\S]*?[?]>`,
761
768
  String.raw `<![A-Za-z][^>]*>`,
762
769
  String.raw `<!\[CDATA\[[\s\S]*?\]\]>`,
763
- ].join("|"), "y");
764
- /** `re`, a sticky pattern, matched exactly at `i` in `text`. */
765
- function matchAt(re, text, i) {
766
- re.lastIndex = i;
767
- return re.exec(text);
770
+ ].join("|"))));
771
+ /** `p` matched exactly at `i` in `text`: read from `i` on, so no regex keeps a cursor, and none is built per call. */
772
+ function matchAt(p, text, i) {
773
+ return p.exec(text.slice(i));
768
774
  }
769
775
  /**
770
776
  * The character a numeric reference to `code` names, unless markdown-it's
@@ -807,11 +813,11 @@ function referenceAt(text, i) {
807
813
  /**
808
814
  * markdown-it's `unescapeAll`, which a link's destination is read through:
809
815
  * each backslash escape as the character it escapes, and each reference
810
- * `UNESCAPE_ALL` finds as the characters it names — a reference naming
816
+ * `unescapes` finds as the characters it names — a reference naming
811
817
  * nothing, or no character `isValidEntityCode` accepts, left as written.
812
818
  */
813
819
  function unescapeAll(text) {
814
- return text.replace(UNESCAPE_ALL, (whole, escaped) => {
820
+ return text.replace(unescapes(), (whole, escaped) => {
815
821
  if (escaped !== undefined)
816
822
  return escaped;
817
823
  const numeric = DESTINATION_NUMERIC.exec(whole);
@@ -893,7 +899,7 @@ class BacktickRuns {
893
899
  // Where each run starts, by its length, in order.
894
900
  starts = new Map();
895
901
  constructor(text) {
896
- for (const run of text.matchAll(TICK_RUNS)) {
902
+ for (const run of text.matchAll(new RegExp(TICKS.source, "g"))) {
897
903
  const starts = this.starts.get(run[0].length);
898
904
  if (starts)
899
905
  starts.push(run.index);
@@ -1227,7 +1233,7 @@ function nextLine(text, from) {
1227
1233
  function inlineAt(text, i, state, level) {
1228
1234
  switch (text[i]) {
1229
1235
  case "`": {
1230
- const ticks = matchAt(TICKS, text, i)[0];
1236
+ const ticks = matchAt(TICKS_AT, text, i)[0];
1231
1237
  const close = state.codeClose(text, i + ticks.length, ticks.length);
1232
1238
  if (close === undefined)
1233
1239
  return { kind: "text", text: ticks, end: i + ticks.length };
@@ -1251,7 +1257,7 @@ function inlineAt(text, i, state, level) {
1251
1257
  // counts toward no hard break.
1252
1258
  if (text[i + 1] === "\n")
1253
1259
  return { kind: "hardbreak", end: nextLine(text, i + 2) };
1254
- const escape = matchAt(ESCAPE, text, i);
1260
+ const escape = matchAt(ESCAPE_AT, text, i);
1255
1261
  if (escape)
1256
1262
  return { kind: "text", text: escape[1], end: i + escape[0].length };
1257
1263
  if (i + 1 === text.length)
@@ -9,13 +9,14 @@
9
9
  * markdown-it keeps the capital, and refuses outright what markdown-it reads.
10
10
  */
11
11
  import { pyStrip } from "./python-space.js";
12
+ import { pattern } from "../core/pattern.js";
12
13
  /** mdurl's protocol, `[a-z0-9.+-]+:` under Python's IGNORECASE, which also folds İ, ı, ſ and K (Kelvin) into `[a-z]`. */
13
- const PROTOCOL = /^[a-z0-9.+\-\u0130\u0131\u017f\u212a]+:/i;
14
- const PORT = /:[0-9]*$/;
14
+ const PROTOCOL = pattern(/^[a-z0-9.+\-\u0130\u0131\u017f\u212a]+:/i);
15
+ const PORT = pattern(/:[0-9]*$/);
15
16
  const HOST_ENDING_CHARS = "/?#";
16
17
  const NON_HOST_CHARS = "%/?;#'{}|\\^`<>\" \r\n\t";
17
- const HOSTNAME_PART = /^[+a-z0-9A-Z_-]{0,63}$/;
18
- const HOSTNAME_PART_START = /^([+a-z0-9A-Z_-]{0,63})(.*)$/s;
18
+ const HOSTNAME_PART = pattern(/^[+a-z0-9A-Z_-]{0,63}$/);
19
+ const HOSTNAME_PART_START = pattern(/^([+a-z0-9A-Z_-]{0,63})(.*)$/s);
19
20
  const HOSTNAME_MAX_LEN = 255;
20
21
  /** Protocols whose hostname markdown-it recodes; anything else may not be a host at all, as `skype:name` is not. */
21
22
  const RECODE_HOSTNAME_FOR = ["http:", "https:", "mailto:"];
@@ -106,8 +107,8 @@ function format(url) {
106
107
  }
107
108
  /** What mdurl's `encode` leaves as it is, besides ASCII letters and digits. */
108
109
  const ENCODE_KEEP = ";/?:@&=+$,-_.!~*'()#";
109
- const ALPHANUMERIC = /[A-Za-z0-9]/;
110
- const HEX_PAIR = /^[0-9a-fA-F]{2}$/;
110
+ const ALPHANUMERIC = pattern(/[A-Za-z0-9]/);
111
+ const HEX_PAIR = pattern(/^[0-9a-fA-F]{2}$/);
111
112
  /** mdurl's `encode`: every character it does not keep percent-encoded as UTF-8, a `%` already starting an escape kept. */
112
113
  function encode(text) {
113
114
  let result = "";
@@ -140,7 +141,6 @@ function encode(text) {
140
141
  }
141
142
  /** What `normalizeLinkText` leaves escaped: mdurl's default reserved set, and `%` (markdown-it issue 720). */
142
143
  const DECODE_KEEP = ";/?:@&=+$,#%";
143
- const ESCAPE_RUN = /(?:%[a-f0-9]{2})+/gi;
144
144
  /**
145
145
  * The code point of the UTF-8 sequence `bytes`, or undefined where Python's
146
146
  * strict decoder refuses it: an overlong form, a surrogate, past U+10FFFF.
@@ -157,7 +157,7 @@ function utf8(bytes) {
157
157
  * and an escape of a kept character left as an escape, its hex uppercased.
158
158
  */
159
159
  function decode(text) {
160
- return text.replace(ESCAPE_RUN, (run) => {
160
+ return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
161
161
  const bytes = run.split("%").slice(1).map((hex) => parseInt(hex, 16));
162
162
  let result = "";
163
163
  for (let i = 0; i < bytes.length;) {
@@ -274,7 +274,7 @@ function punycodeDecode(label) {
274
274
  return output.join("");
275
275
  }
276
276
  /** A host's labels, split at any of the four full stops IDNA reads as one, and joined back with the ASCII one. */
277
- const LABEL_SEPARATOR = /[.。.。]/;
277
+ const LABEL_SEPARATOR = pattern(/[.。.。]/);
278
278
  /** markdown-it's `to_ascii`: each label carrying a non-ASCII character punycoded. A hostname carries no `@`, so the email half of its `map_domain` never applies. */
279
279
  function toAscii(hostname) {
280
280
  return hostname
@@ -295,8 +295,8 @@ function recodeHostname(url, recode) {
295
295
  }
296
296
  return format(parsed);
297
297
  }
298
- const BAD_PROTOCOL = /^(?:vbscript|javascript|file|data):/;
299
- const GOOD_DATA = /^data:image\/(?:gif|png|jpeg|webp);/;
298
+ const BAD_PROTOCOL = pattern(/^(?:vbscript|javascript|file|data):/);
299
+ const GOOD_DATA = pattern(/^data:image\/(?:gif|png|jpeg|webp);/);
300
300
  /**
301
301
  * The href markdown-it gives a link to `destination`: its host punycoded and
302
302
  * the rest percent-encoded, as `normalizeLink` does — or undefined where
@@ -4,8 +4,8 @@
4
4
  import { Segment } from "../core/segment.js";
5
5
  import { Style } from "../core/style.js";
6
6
  import type { RichText } from "../core/text.js";
7
- import type { Composite, Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
- export type PaddingDimensions = number | [number, number] | [number, number, number, number];
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
+ export type PaddingDimensions = number | readonly [number, number] | readonly [number, number, number, number];
9
9
  /**
10
10
  * Parses the shapes a caller may write padding in — one number, a vertical/
11
11
  * horizontal pair, or all four sides — into the four-sided form every
@@ -18,7 +18,7 @@ export type PaddingDimensions = number | [number, number] | [number, number, num
18
18
  * to reach the renderers, where `Panel` drew content rows wider than its own
19
19
  * border and `Table` threw outright.
20
20
  */
21
- export declare function normalizePadding(padding: PaddingDimensions): [number, number, number, number];
21
+ export declare function normalizePadding(padding: PaddingDimensions): readonly [number, number, number, number];
22
22
  /**
23
23
  * The width a padding leaves its content: what the request holds once both
24
24
  * sides are paid for, as Rich's `Padding` divides it. The sides are paid in
@@ -26,7 +26,7 @@ export declare function normalizePadding(padding: PaddingDimensions): [number, n
26
26
  * is drawn as nothing — Rich's `Console.render` under one column.
27
27
  */
28
28
  export declare function paddedContentWidth(outerWidth: number, left: number, right: number): number;
29
- export declare class Padding implements Renderable, Measurable, Composite {
29
+ export declare class Padding implements Renderable, Measurable {
30
30
  readonly renderable: Renderable;
31
31
  readonly top: number;
32
32
  readonly right: number;
@@ -38,7 +38,6 @@ export declare class Padding implements Renderable, Measurable, Composite {
38
38
  style?: string | Style;
39
39
  expand?: boolean;
40
40
  });
41
- contents(): Iterable<Renderable>;
42
41
  render(rawOptions: RenderOptions): Iterable<Segment>;
43
42
  measure(rawOptions: RenderOptions): {
44
43
  minimum: number;
@@ -67,9 +67,6 @@ export class Padding {
67
67
  this.style = options?.style ?? NULL_STYLE;
68
68
  this.expand = options?.expand !== false;
69
69
  }
70
- *contents() {
71
- yield this.renderable;
72
- }
73
70
  *render(rawOptions) {
74
71
  const options = withBoundedWidth(rawOptions, this);
75
72
  // `expand` decides how wide the block is, never whether its rows reach