@promptctl/rich-js 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
package/dist/core/text.js
CHANGED
|
@@ -6,17 +6,28 @@ import { divideLine, wordWidths } from "./wrap.js";
|
|
|
6
6
|
import { Segment } from "./segment.js";
|
|
7
7
|
import { Style, NULL_STYLE, StyleSyntaxError } from "./style.js";
|
|
8
8
|
import { stripOscTerminators } from "./osc8.js";
|
|
9
|
-
import { drawable, getStyle, withBoundedWidth } from "./protocol.js";
|
|
9
|
+
import { betweenPasses, drawable, getStyle, withBoundedWidth } from "./protocol.js";
|
|
10
|
+
import { pattern } from "./pattern.js";
|
|
11
|
+
import { ListState, State } from "./state.js";
|
|
10
12
|
// Every C0 control and DEL except \t and \n. Rich's `Text` strips only
|
|
11
13
|
// `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR); the rest of the set is the
|
|
12
|
-
// port's extension, so no
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
14
|
+
// port's extension, so no byte of content can start an escape sequence or move
|
|
15
|
+
// the cursor on the wire: the C1 controls (U+0080–U+009F) go as well, since a
|
|
16
|
+
// terminal reads U+009B as CSI and U+009D as OSC (TextInput refuses them for
|
|
17
|
+
// the same reason). So do the format controls that hold state past the text,
|
|
18
|
+
// also the port's extension: the bidi embeddings and overrides with their
|
|
19
|
+
// closer PDF (U+202A–U+202E), the isolates with their closer PDI
|
|
20
|
+
// (U+2066–U+2069), and the deprecated shaping and swapping switches
|
|
21
|
+
// (U+206A–U+206F). An opener left unclosed in content reverses whatever is
|
|
22
|
+
// drawn after it on the line, and a closer whose opener is gone closes
|
|
23
|
+
// nothing of the text's own. The bidi marks (LRM, RLM, ALM) hold no state and
|
|
24
|
+
// pass, since right-to-left text needs them.
|
|
25
|
+
// [LAW:single-enforcer] Single place where control chars are sanitized: every
|
|
26
|
+
// string that becomes part of a Text's characters passes here.
|
|
16
27
|
export function stripControlChars(text) {
|
|
17
|
-
return text.replace(
|
|
28
|
+
return text.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u206F]/g, "");
|
|
18
29
|
}
|
|
19
|
-
const TRAILING_WHITESPACE_RE = /\s
|
|
30
|
+
const TRAILING_WHITESPACE_RE = pattern(/\s+$/);
|
|
20
31
|
/**
|
|
21
32
|
* The plain text of one line of segments, in the coordinate system
|
|
22
33
|
* `divideLine` and `Segment.divide` share: cell offsets into the styled line
|
|
@@ -74,9 +85,9 @@ function sanitizeStyleLink(style) {
|
|
|
74
85
|
* lays its text out anew, so a stamp naming where a cell sat in the output it
|
|
75
86
|
* came from would name the wrong cell once it wraps, slices or overlaps; the
|
|
76
87
|
* owner that renders the RichText is the one that stamps its cells. A
|
|
77
|
-
* `
|
|
78
|
-
* wherever the text lays them, so a frame
|
|
79
|
-
* when it turns.
|
|
88
|
+
* hold stays — `lastChange` and `nextChange`: when a span's cells began and
|
|
89
|
+
* next change is true of them wherever the text lays them, so a frame
|
|
90
|
+
* appended into a line still says when it turns.
|
|
80
91
|
*/
|
|
81
92
|
function admitStyle(style) {
|
|
82
93
|
if (typeof style === "string")
|
|
@@ -119,7 +130,7 @@ function resolveDefinition(options, style) {
|
|
|
119
130
|
catch (err) {
|
|
120
131
|
if (!(err instanceof StyleSyntaxError))
|
|
121
132
|
throw err;
|
|
122
|
-
options.onStyleError?.(err, style);
|
|
133
|
+
betweenPasses(() => options.onStyleError?.(err, style));
|
|
123
134
|
return NULL_STYLE;
|
|
124
135
|
}
|
|
125
136
|
}
|
|
@@ -160,41 +171,45 @@ export class Span {
|
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
173
|
export class RichText {
|
|
174
|
+
// Rich's `Text` is changed in place — appended to, stylized, padded — so its
|
|
175
|
+
// characters, spans and format are state, and a write to one while
|
|
176
|
+
// something is drawn throws. Three holders rather than one per field: a
|
|
177
|
+
// `RichText` is the most-made object in the library.
|
|
163
178
|
_text;
|
|
164
|
-
_spans;
|
|
165
|
-
|
|
166
|
-
_justify;
|
|
167
|
-
_overflow;
|
|
168
|
-
_end;
|
|
179
|
+
_spans = new ListState();
|
|
180
|
+
_format;
|
|
169
181
|
_tabSize;
|
|
170
|
-
_noWrap;
|
|
171
182
|
constructor(text, options) {
|
|
172
|
-
this._text = text ? stripControlChars(text) : "";
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
this._text = new State(text ? stripControlChars(text) : "");
|
|
184
|
+
this._format = new State({
|
|
185
|
+
// [LAW:single-enforcer] `admitStyle` is the boundary that sanitizes
|
|
186
|
+
// any link URL crossing into a RichText; downstream trusts the invariant.
|
|
187
|
+
style: admitStyle(options?.style ?? NULL_STYLE),
|
|
188
|
+
justify: options?.justify,
|
|
189
|
+
overflow: options?.overflow,
|
|
190
|
+
end: options?.end ?? "\n",
|
|
191
|
+
noWrap: options?.noWrap ?? false,
|
|
192
|
+
});
|
|
180
193
|
this._tabSize = parseTabSize(options?.tabSize ?? 8);
|
|
181
|
-
|
|
194
|
+
}
|
|
195
|
+
_reformat(change) {
|
|
196
|
+
this._format.set({ ...this._format.get(), ...change });
|
|
182
197
|
}
|
|
183
198
|
// --- Properties ---
|
|
184
199
|
get plain() {
|
|
185
|
-
return this._text;
|
|
200
|
+
return this._text.get();
|
|
186
201
|
}
|
|
187
202
|
set plain(value) {
|
|
188
203
|
const sanitized = stripControlChars(value);
|
|
189
|
-
this._text
|
|
204
|
+
this._text.set(sanitized);
|
|
190
205
|
// Trim spans that extend beyond new length
|
|
191
206
|
const len = sanitized.length;
|
|
192
|
-
this._spans
|
|
207
|
+
this._spans.set(this._spans.get()
|
|
193
208
|
.map((s) => (s.end > len ? new Span(s.start, Math.min(s.end, len), s.style) : s))
|
|
194
|
-
.filter((s) => s.start < len);
|
|
209
|
+
.filter((s) => s.start < len));
|
|
195
210
|
}
|
|
196
211
|
get length() {
|
|
197
|
-
return this._text.length;
|
|
212
|
+
return this._text.get().length;
|
|
198
213
|
}
|
|
199
214
|
/**
|
|
200
215
|
* The cells of the plain text with each tab widened to its stop counted from
|
|
@@ -205,44 +220,44 @@ export class RichText {
|
|
|
205
220
|
* no render draws.
|
|
206
221
|
*/
|
|
207
222
|
get cellLength() {
|
|
208
|
-
return cellLen(expandTabs(this._text, this._tabSize).text);
|
|
223
|
+
return cellLen(expandTabs(this._text.get(), this._tabSize).text);
|
|
209
224
|
}
|
|
210
225
|
get hasContent() {
|
|
211
|
-
return this._text.length > 0;
|
|
226
|
+
return this._text.get().length > 0;
|
|
212
227
|
}
|
|
213
228
|
/** The base style every span layers over, resolved at render unless it is a `Style`. */
|
|
214
229
|
get style() {
|
|
215
|
-
return this.
|
|
230
|
+
return this._format.get().style;
|
|
216
231
|
}
|
|
217
232
|
set style(value) {
|
|
218
|
-
this.
|
|
233
|
+
this._reformat({ style: admitStyle(value) });
|
|
219
234
|
}
|
|
220
235
|
get justify() {
|
|
221
|
-
return this.
|
|
236
|
+
return this._format.get().justify;
|
|
222
237
|
}
|
|
223
238
|
set justify(value) {
|
|
224
|
-
this.
|
|
239
|
+
this._reformat({ justify: value });
|
|
225
240
|
}
|
|
226
241
|
get overflow() {
|
|
227
|
-
return this.
|
|
242
|
+
return this._format.get().overflow;
|
|
228
243
|
}
|
|
229
244
|
set overflow(value) {
|
|
230
|
-
this.
|
|
245
|
+
this._reformat({ overflow: value });
|
|
231
246
|
}
|
|
232
247
|
get end() {
|
|
233
|
-
return this.
|
|
248
|
+
return this._format.get().end;
|
|
234
249
|
}
|
|
235
250
|
set end(value) {
|
|
236
|
-
this.
|
|
251
|
+
this._reformat({ end: value });
|
|
237
252
|
}
|
|
238
253
|
get noWrap() {
|
|
239
|
-
return this.
|
|
254
|
+
return this._format.get().noWrap;
|
|
240
255
|
}
|
|
241
256
|
set noWrap(value) {
|
|
242
|
-
this.
|
|
257
|
+
this._reformat({ noWrap: value });
|
|
243
258
|
}
|
|
244
259
|
get spans() {
|
|
245
|
-
return this._spans;
|
|
260
|
+
return this._spans.get();
|
|
246
261
|
}
|
|
247
262
|
/**
|
|
248
263
|
* This text's own style, spans aside, as the render drawing it resolves it.
|
|
@@ -250,7 +265,7 @@ export class RichText {
|
|
|
250
265
|
* text forgives a missing name.
|
|
251
266
|
*/
|
|
252
267
|
resolvedStyle(options) {
|
|
253
|
-
return resolveStyle(options, this.
|
|
268
|
+
return resolveStyle(options, this._format.get().style);
|
|
254
269
|
}
|
|
255
270
|
/**
|
|
256
271
|
* The style of the cell-column at the named edge — base style merged with
|
|
@@ -274,11 +289,11 @@ export class RichText {
|
|
|
274
289
|
*/
|
|
275
290
|
edgeStyle(side, options) {
|
|
276
291
|
const base = this.resolvedStyle(options);
|
|
277
|
-
if (this._text.length === 0)
|
|
292
|
+
if (this._text.get().length === 0)
|
|
278
293
|
return base;
|
|
279
|
-
const pos = side === "left" ? 0 : this._text.length - 1;
|
|
294
|
+
const pos = side === "left" ? 0 : this._text.get().length - 1;
|
|
280
295
|
let result = base;
|
|
281
|
-
for (const span of this._spans) {
|
|
296
|
+
for (const span of this._spans.get()) {
|
|
282
297
|
if (span.start <= pos && pos < span.end) {
|
|
283
298
|
result = result.add(resolveStyle(options, span.style));
|
|
284
299
|
}
|
|
@@ -291,35 +306,36 @@ export class RichText {
|
|
|
291
306
|
if (style !== undefined) {
|
|
292
307
|
throw new Error("Style argument must not be provided when appending RichText");
|
|
293
308
|
}
|
|
294
|
-
const offset = this._text.length;
|
|
295
|
-
|
|
309
|
+
const offset = this._text.get().length;
|
|
310
|
+
// Read before anything is appended, so a text appended to itself takes its spans as they were.
|
|
311
|
+
const moved = content._spans.get().map((span) => span.move(offset));
|
|
312
|
+
this._text.set(this._text.get() + content._text.get());
|
|
296
313
|
// Its base style becomes a span under its own spans, as the reference's
|
|
297
314
|
// `append` does, so appended text looks as it did alone.
|
|
298
|
-
this._addSpan(offset, this._text.length, content.
|
|
299
|
-
for (const span of
|
|
300
|
-
this._spans.push(span
|
|
301
|
-
}
|
|
315
|
+
this._addSpan(offset, this._text.get().length, content._format.get().style);
|
|
316
|
+
for (const span of moved)
|
|
317
|
+
this._spans.push(span);
|
|
302
318
|
return this;
|
|
303
319
|
}
|
|
304
320
|
const sanitized = stripControlChars(content);
|
|
305
|
-
const start = this._text.length;
|
|
306
|
-
this._text
|
|
307
|
-
this._addSpan(start, this._text.length, style ?? "");
|
|
321
|
+
const start = this._text.get().length;
|
|
322
|
+
this._text.set(this._text.get() + sanitized);
|
|
323
|
+
this._addSpan(start, this._text.get().length, style ?? "");
|
|
308
324
|
return this;
|
|
309
325
|
}
|
|
310
326
|
contains(needle) {
|
|
311
|
-
const searchText = needle instanceof RichText ? needle._text : needle;
|
|
312
|
-
return this._text.includes(searchText);
|
|
327
|
+
const searchText = needle instanceof RichText ? needle._text.get() : needle;
|
|
328
|
+
return this._text.get().includes(searchText);
|
|
313
329
|
}
|
|
314
330
|
at(index) {
|
|
315
|
-
const resolved = index < 0 ? this._text.length + index : index;
|
|
316
|
-
const char = this._text[resolved];
|
|
331
|
+
const resolved = index < 0 ? this._text.get().length + index : index;
|
|
332
|
+
const char = this._text.get()[resolved];
|
|
317
333
|
if (char === undefined)
|
|
318
334
|
return new RichText("");
|
|
319
335
|
return this.slice(resolved, resolved + 1);
|
|
320
336
|
}
|
|
321
337
|
slice(start, end) {
|
|
322
|
-
const text = this._text;
|
|
338
|
+
const text = this._text.get();
|
|
323
339
|
const len = text.length;
|
|
324
340
|
const s = start ?? 0;
|
|
325
341
|
const e = end ?? len;
|
|
@@ -330,7 +346,7 @@ export class RichText {
|
|
|
330
346
|
}
|
|
331
347
|
const slicedText = text.slice(resolvedStart, resolvedEnd);
|
|
332
348
|
const result = this.blankCopy(slicedText);
|
|
333
|
-
for (const span of this._spans) {
|
|
349
|
+
for (const span of this._spans.get()) {
|
|
334
350
|
const spanStart = Math.max(span.start, resolvedStart) - resolvedStart;
|
|
335
351
|
const spanEnd = Math.min(span.end, resolvedEnd) - resolvedStart;
|
|
336
352
|
if (spanStart < spanEnd) {
|
|
@@ -352,7 +368,7 @@ export class RichText {
|
|
|
352
368
|
this._spans.push(new Span(start, end, admitted));
|
|
353
369
|
}
|
|
354
370
|
stylize(style, start, end) {
|
|
355
|
-
const len = this._text.length;
|
|
371
|
+
const len = this._text.get().length;
|
|
356
372
|
const s = start !== undefined ? (start < 0 ? len + start : start) : 0;
|
|
357
373
|
const e = end !== undefined ? (end < 0 ? len + end : end) : len;
|
|
358
374
|
if (s >= e || s >= len || e <= 0)
|
|
@@ -363,7 +379,7 @@ export class RichText {
|
|
|
363
379
|
return this;
|
|
364
380
|
}
|
|
365
381
|
highlightRegex(pattern, style) {
|
|
366
|
-
const text = this._text;
|
|
382
|
+
const text = this._text.get();
|
|
367
383
|
let count = 0;
|
|
368
384
|
// Ensure global flag
|
|
369
385
|
const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
|
|
@@ -405,7 +421,7 @@ export class RichText {
|
|
|
405
421
|
const flags = caseSensitive ? "g" : "gi";
|
|
406
422
|
const re = new RegExp(`\\b${escaped}\\b`, flags);
|
|
407
423
|
let match;
|
|
408
|
-
while ((match = re.exec(this._text)) !== null) {
|
|
424
|
+
while ((match = re.exec(this._text.get())) !== null) {
|
|
409
425
|
this._addSpan(match.index, match.index + match[0].length, style);
|
|
410
426
|
count++;
|
|
411
427
|
}
|
|
@@ -414,31 +430,17 @@ export class RichText {
|
|
|
414
430
|
}
|
|
415
431
|
// --- Copy Operations ---
|
|
416
432
|
copy() {
|
|
417
|
-
const result =
|
|
418
|
-
|
|
419
|
-
justify: this._justify,
|
|
420
|
-
overflow: this._overflow,
|
|
421
|
-
end: this._end,
|
|
422
|
-
tabSize: this._tabSize,
|
|
423
|
-
noWrap: this._noWrap,
|
|
424
|
-
});
|
|
425
|
-
result._spans = this._spans.slice();
|
|
433
|
+
const result = this.blankCopy(this._text.get());
|
|
434
|
+
result._spans.set(this._spans.get());
|
|
426
435
|
return result;
|
|
427
436
|
}
|
|
428
437
|
blankCopy(text) {
|
|
429
|
-
return new RichText(text ?? "", {
|
|
430
|
-
style: this._style,
|
|
431
|
-
justify: this._justify,
|
|
432
|
-
overflow: this._overflow,
|
|
433
|
-
end: this._end,
|
|
434
|
-
tabSize: this._tabSize,
|
|
435
|
-
noWrap: this._noWrap,
|
|
436
|
-
});
|
|
438
|
+
return new RichText(text ?? "", { ...this._format.get(), tabSize: this._tabSize });
|
|
437
439
|
}
|
|
438
440
|
// --- Splitting ---
|
|
439
441
|
split(separator) {
|
|
440
442
|
const sep = separator ?? "\n";
|
|
441
|
-
const text = this._text;
|
|
443
|
+
const text = this._text.get();
|
|
442
444
|
const parts = [];
|
|
443
445
|
let start = 0;
|
|
444
446
|
while (true) {
|
|
@@ -466,18 +468,17 @@ export class RichText {
|
|
|
466
468
|
}
|
|
467
469
|
// --- Whitespace Operations ---
|
|
468
470
|
rstrip() {
|
|
469
|
-
const trimmed = this._text.replace(/\s+$/, "");
|
|
470
|
-
if (trimmed.length < this._text.length) {
|
|
471
|
+
const trimmed = this._text.get().replace(/\s+$/, "");
|
|
472
|
+
if (trimmed.length < this._text.get().length) {
|
|
471
473
|
this.plain = trimmed;
|
|
472
474
|
}
|
|
473
475
|
return this;
|
|
474
476
|
}
|
|
475
477
|
pad(count, char) {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this.
|
|
480
|
-
this._text = padding + this._text + padding;
|
|
478
|
+
const padding = stripControlChars(char ?? " ").repeat(count);
|
|
479
|
+
// Shift all spans right by what the left side gained.
|
|
480
|
+
this._spans.set(this._spans.get().map((s) => s.move(padding.length)));
|
|
481
|
+
this._text.set(padding + this._text.get() + padding);
|
|
481
482
|
return this;
|
|
482
483
|
}
|
|
483
484
|
/**
|
|
@@ -486,36 +487,35 @@ export class RichText {
|
|
|
486
487
|
* the text measures what it will draw.
|
|
487
488
|
*/
|
|
488
489
|
expandTabs() {
|
|
489
|
-
const expanded = expandTabs(this._text, this._tabSize);
|
|
490
|
-
this._spans
|
|
491
|
-
this._text
|
|
490
|
+
const expanded = expandTabs(this._text.get(), this._tabSize);
|
|
491
|
+
this._spans.set(this._spans.get().map((s) => new Span(expanded.offsetOf(s.start), expanded.offsetOf(s.end), s.style)));
|
|
492
|
+
this._text.set(expanded.text);
|
|
492
493
|
return this;
|
|
493
494
|
}
|
|
494
495
|
padLeft(count, char) {
|
|
495
|
-
const
|
|
496
|
-
this._spans
|
|
497
|
-
this._text
|
|
496
|
+
const padding = stripControlChars(char ?? " ").repeat(count);
|
|
497
|
+
this._spans.set(this._spans.get().map((s) => s.move(padding.length)));
|
|
498
|
+
this._text.set(padding + this._text.get());
|
|
498
499
|
return this;
|
|
499
500
|
}
|
|
500
501
|
padRight(count, char) {
|
|
501
|
-
|
|
502
|
-
this._text += c.repeat(count);
|
|
502
|
+
this._text.set(this._text.get() + stripControlChars(char ?? " ").repeat(count));
|
|
503
503
|
return this;
|
|
504
504
|
}
|
|
505
505
|
setLength(length) {
|
|
506
|
-
if (this._text.length < length) {
|
|
507
|
-
this._text
|
|
506
|
+
if (this._text.get().length < length) {
|
|
507
|
+
this._text.set(this._text.get() + " ".repeat(length - this._text.get().length));
|
|
508
508
|
}
|
|
509
|
-
else if (this._text.length > length) {
|
|
510
|
-
this.plain = this._text.slice(0, length);
|
|
509
|
+
else if (this._text.get().length > length) {
|
|
510
|
+
this.plain = this._text.get().slice(0, length);
|
|
511
511
|
}
|
|
512
512
|
return this;
|
|
513
513
|
}
|
|
514
514
|
extendStyle(count) {
|
|
515
|
-
const oldLen = this._text.length;
|
|
516
|
-
this._text
|
|
515
|
+
const oldLen = this._text.get().length;
|
|
516
|
+
this._text.set(this._text.get() + " ".repeat(count));
|
|
517
517
|
// Extend existing spans to cover new spaces
|
|
518
|
-
this._spans
|
|
518
|
+
this._spans.set(this._spans.get().map((s) => s.end === oldLen ? s.extend(count) : s));
|
|
519
519
|
return this;
|
|
520
520
|
}
|
|
521
521
|
// --- Truncation ---
|
|
@@ -558,19 +558,19 @@ export class RichText {
|
|
|
558
558
|
// value, so width 0 and a marker wider than the width run the same walk
|
|
559
559
|
// as every other call: what is drawn plus what is kept is `cells` wide.
|
|
560
560
|
const cells = cellCount(width);
|
|
561
|
-
const marker = cellFit(options?.marker ?? "\u2026", cells);
|
|
561
|
+
const marker = cellFit(stripControlChars(options?.marker ?? "\u2026"), cells);
|
|
562
562
|
const budget = asCellCol(cells - cellLen(marker));
|
|
563
563
|
if (mode === "right") {
|
|
564
564
|
this._cropRightTo(budget);
|
|
565
565
|
if (marker)
|
|
566
|
-
this._text
|
|
566
|
+
this._text.set(this._text.get() + marker);
|
|
567
567
|
return this;
|
|
568
568
|
}
|
|
569
569
|
if (mode === "left") {
|
|
570
570
|
this._cropLeftTo(budget);
|
|
571
571
|
if (marker) {
|
|
572
|
-
this._spans
|
|
573
|
-
this._text
|
|
572
|
+
this._spans.set(this._spans.get().map((s) => s.move(marker.length)));
|
|
573
|
+
this._text.set(marker + this._text.get());
|
|
574
574
|
}
|
|
575
575
|
return this;
|
|
576
576
|
}
|
|
@@ -578,17 +578,17 @@ export class RichText {
|
|
|
578
578
|
const leftBudget = asCellCol(Math.floor(budget / 2));
|
|
579
579
|
const rightBudget = asCellCol(budget - leftBudget);
|
|
580
580
|
// Find the char-index ranges to keep from each side.
|
|
581
|
-
const leftEndCharIdx = cellFit(this._text, leftBudget).length;
|
|
582
|
-
const rightStartCharIdx = this._text.length - cellFitEnd(this._text, rightBudget).length;
|
|
583
|
-
const leftText = this._text.slice(0, leftEndCharIdx);
|
|
584
|
-
const rightText = this._text.slice(rightStartCharIdx);
|
|
581
|
+
const leftEndCharIdx = cellFit(this._text.get(), leftBudget).length;
|
|
582
|
+
const rightStartCharIdx = this._text.get().length - cellFitEnd(this._text.get(), rightBudget).length;
|
|
583
|
+
const leftText = this._text.get().slice(0, leftEndCharIdx);
|
|
584
|
+
const rightText = this._text.get().slice(rightStartCharIdx);
|
|
585
585
|
const droppedStart = leftEndCharIdx;
|
|
586
586
|
const droppedEnd = rightStartCharIdx;
|
|
587
587
|
const shift = marker.length - (droppedEnd - droppedStart);
|
|
588
588
|
// Spans falling entirely before the drop stay; entirely after shift by
|
|
589
589
|
// (marker.length - dropped chars); spans crossing the drop are clipped.
|
|
590
590
|
const newSpans = [];
|
|
591
|
-
for (const s of this._spans) {
|
|
591
|
+
for (const s of this._spans.get()) {
|
|
592
592
|
if (s.end <= droppedStart) {
|
|
593
593
|
newSpans.push(s);
|
|
594
594
|
}
|
|
@@ -605,18 +605,18 @@ export class RichText {
|
|
|
605
605
|
}
|
|
606
606
|
}
|
|
607
607
|
}
|
|
608
|
-
this._text
|
|
609
|
-
this._spans
|
|
608
|
+
this._text.set(leftText + marker + rightText);
|
|
609
|
+
this._spans.set(newSpans);
|
|
610
610
|
return this;
|
|
611
611
|
}
|
|
612
612
|
_cropRightTo(targetWidth) {
|
|
613
|
-
this.plain = cellFit(this._text, targetWidth);
|
|
613
|
+
this.plain = cellFit(this._text.get(), targetWidth);
|
|
614
614
|
}
|
|
615
615
|
_cropLeftTo(targetWidth) {
|
|
616
|
-
const charIdx = this._text.length - cellFitEnd(this._text, targetWidth).length;
|
|
616
|
+
const charIdx = this._text.get().length - cellFitEnd(this._text.get(), targetWidth).length;
|
|
617
617
|
// Shift spans left by charIdx; clip spans that started before.
|
|
618
618
|
const shift = -charIdx;
|
|
619
|
-
this._spans
|
|
619
|
+
this._spans.set(this._spans.get()
|
|
620
620
|
.map((s) => {
|
|
621
621
|
if (s.end <= charIdx)
|
|
622
622
|
return undefined;
|
|
@@ -624,8 +624,8 @@ export class RichText {
|
|
|
624
624
|
const newEnd = s.end + shift;
|
|
625
625
|
return new Span(newStart, newEnd, s.style);
|
|
626
626
|
})
|
|
627
|
-
.filter((s) => s !== undefined);
|
|
628
|
-
this._text
|
|
627
|
+
.filter((s) => s !== undefined));
|
|
628
|
+
this._text.set(this._text.get().slice(charIdx));
|
|
629
629
|
}
|
|
630
630
|
// --- Alignment ---
|
|
631
631
|
align(justify, width) {
|
|
@@ -655,8 +655,8 @@ export class RichText {
|
|
|
655
655
|
}
|
|
656
656
|
// --- Suffix Removal ---
|
|
657
657
|
removeSuffix(suffix) {
|
|
658
|
-
if (this._text.endsWith(suffix)) {
|
|
659
|
-
this.plain = this._text.slice(0, -suffix.length);
|
|
658
|
+
if (this._text.get().endsWith(suffix)) {
|
|
659
|
+
this.plain = this._text.get().slice(0, -suffix.length);
|
|
660
660
|
}
|
|
661
661
|
return this;
|
|
662
662
|
}
|
|
@@ -713,7 +713,7 @@ export class RichText {
|
|
|
713
713
|
}
|
|
714
714
|
// --- Renderable ---
|
|
715
715
|
*render(options) {
|
|
716
|
-
const expanded = expandTabs(this._text, this._tabSize);
|
|
716
|
+
const expanded = expandTabs(this._text.get(), this._tabSize);
|
|
717
717
|
const base = this.resolvedStyle(options);
|
|
718
718
|
const { segments: allSegments, covered } = this._buildSegments(expanded, base, options);
|
|
719
719
|
const spanFree = spanFreeLines(allSegments, covered);
|
|
@@ -731,8 +731,8 @@ export class RichText {
|
|
|
731
731
|
// overflow arm a no-op, but passed an unbounded one through to `justify`,
|
|
732
732
|
// which pads — and `" ".repeat(Infinity)` throws.
|
|
733
733
|
const maxWidth = cellCount(withBoundedWidth(options, this).maxWidth);
|
|
734
|
-
const overflow = this.
|
|
735
|
-
const noWrap = this.
|
|
734
|
+
const overflow = this._format.get().overflow ?? options.overflow ?? "fold";
|
|
735
|
+
const noWrap = this._format.get().noWrap || (options.noWrap ?? false);
|
|
736
736
|
// Two widths, and they are the reference's `no_wrap` and `"ignore"` told
|
|
737
737
|
// apart. `edge` is where a line is cut: gone under `"ignore"`, so the line
|
|
738
738
|
// leaves at its natural width and whatever drew it decides about the
|
|
@@ -751,7 +751,7 @@ export class RichText {
|
|
|
751
751
|
// A line with no edge has no width to be justified in, so under `"ignore"`
|
|
752
752
|
// it is not justified — the reference's `Text.wrap` returns that arm before
|
|
753
753
|
// `justify`. `undefined` is the placement that leaves a line as it is.
|
|
754
|
-
const justify = overflow === "ignore" ? undefined : this.
|
|
754
|
+
const justify = overflow === "ignore" ? undefined : this._format.get().justify ?? options.justify;
|
|
755
755
|
// One cell either way, so the marker's stand-in leaves the cut where it was.
|
|
756
756
|
const ellipsis = drawable(options, "\u2026", ".");
|
|
757
757
|
for (let index = 0; index < logicalLines.length; index += 1) {
|
|
@@ -779,13 +779,13 @@ export class RichText {
|
|
|
779
779
|
// (truthy) and again for non-empty text (truthy and not the default
|
|
780
780
|
// "\n"), which gave `end` two meanings depending on whether the text was
|
|
781
781
|
// empty (rich-text-5ai).
|
|
782
|
-
if (this.
|
|
783
|
-
yield new Segment(this.
|
|
782
|
+
if (this._format.get().end) {
|
|
783
|
+
yield new Segment(this._format.get().end);
|
|
784
784
|
}
|
|
785
785
|
}
|
|
786
786
|
// --- Measurable ---
|
|
787
787
|
measure(options) {
|
|
788
|
-
const lines = expandTabs(this._text, this._tabSize).text.split("\n");
|
|
788
|
+
const lines = expandTabs(this._text.get(), this._tabSize).text.split("\n");
|
|
789
789
|
const maxLineWidth = lines.reduce((widest, line) => Math.max(widest, cellLen(line)), 0);
|
|
790
790
|
// [LAW:one-source-of-truth] The minimum is the widest piece the wrapper
|
|
791
791
|
// never breaks, asked of the wrapper — so a line's indent, which wraps
|
|
@@ -840,7 +840,7 @@ export class RichText {
|
|
|
840
840
|
// and it is what makes empty text one line to everything that splits a
|
|
841
841
|
// render into lines — a panel's body, a table cell — where Rich draws one.
|
|
842
842
|
const inner = new Set();
|
|
843
|
-
for (const span of this._spans) {
|
|
843
|
+
for (const span of this._spans.get()) {
|
|
844
844
|
inner.add(clamp(span.start));
|
|
845
845
|
inner.add(clamp(span.end));
|
|
846
846
|
}
|
|
@@ -854,7 +854,7 @@ export class RichText {
|
|
|
854
854
|
const pieceAt = new Map(boundaries.map((position, piece) => [position, piece]).reverse());
|
|
855
855
|
const styles = boundaries.slice(0, -1).map(() => base);
|
|
856
856
|
const covered = styles.map(() => false);
|
|
857
|
-
for (const span of this._spans) {
|
|
857
|
+
for (const span of this._spans.get()) {
|
|
858
858
|
const end = clamp(span.end);
|
|
859
859
|
const style = resolveStyle(options, span.style);
|
|
860
860
|
const opensAt = pieceAt.get(clamp(span.start));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline — the map from the host's clock to the time an animation is
|
|
3
|
+
* asked for: playback control, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Every animation is a pure function of its time (`./animation.ts`), so
|
|
6
|
+
* pausing, reversing, scrubbing and speeding one up are not things done to
|
|
7
|
+
* it but a change in which time it is asked for. A timeline is that map, as
|
|
8
|
+
* a value: an animation time at a host time, a rate — animation seconds per
|
|
9
|
+
* host second, negative running it backwards — and whether it is held. Each
|
|
10
|
+
* operation is a value in and a value out, so a paused timeline resumed at a
|
|
11
|
+
* new host time is one that maps that host time to the time it froze at, and
|
|
12
|
+
* two hosts holding equal timelines ask every animation for the same time.
|
|
13
|
+
*
|
|
14
|
+
* A paused timeline carries no host anchor: it maps every host time to the
|
|
15
|
+
* one time it holds, so there is no origin for it to be wrong about.
|
|
16
|
+
* [LAW:types-are-the-program]
|
|
17
|
+
*
|
|
18
|
+
* `run` is an animation on host time. Its `nextChange` is the host moment
|
|
19
|
+
* the mapped time reaches the animation's own answer, which is what
|
|
20
|
+
* `Spinner`'s speed scaled its holds for before this existed: an answer a
|
|
21
|
+
* host reads is on the host's timeline, and a beat over scaled time answers
|
|
22
|
+
* on the scaled one. Run backwards, the host next sees a change when the
|
|
23
|
+
* mapped time drops under where the value began — the animation's
|
|
24
|
+
* `lastChange` — so a reversed loop turns exactly at its hold boundaries,
|
|
25
|
+
* and a reversed one-shot changes at its switch and never before its start.
|
|
26
|
+
* Paused, nothing changes: never, and never did. The host moment is the
|
|
27
|
+
* first host float the map carries past the edge, searched out from the sum
|
|
28
|
+
* `origin + (edge - time) / rate` (`firstReaching`): a host float is a step
|
|
29
|
+
* of `rate` on the animation's clock, so the floats between that sum and
|
|
30
|
+
* the first one past the edge are as many as the two clocks' resolutions
|
|
31
|
+
* are apart — unbounded, far from the origin or at a slow rate — which is
|
|
32
|
+
* why it is a search and not the ulp walk a sum on one clock takes. A value
|
|
33
|
+
* moving at the mapped time changes at the host time itself.
|
|
34
|
+
*
|
|
35
|
+
* A stagger — a Group's children pulsing in sequence, a light walking down
|
|
36
|
+
* a Table's rows — is the same map once per child: `stagger` runs each
|
|
37
|
+
* child's animation on a timeline playing from its index times the delay,
|
|
38
|
+
* and the group changes when the soonest child does, each child's answer
|
|
39
|
+
* carried onto the group's clock by `run`. Before its turn a child is asked
|
|
40
|
+
* for a time before 0, which is its own business: a loop is shifted in
|
|
41
|
+
* phase. Tree, Table, Group and Layout take the children's frames as they
|
|
42
|
+
* take any renderables, through `Animation.map`, so none of them knows a
|
|
43
|
+
* stagger exists. [LAW:composability] A lifecycle is not staggered here: it
|
|
44
|
+
* carries its turn as `entered`, on the host's clock its `leave` and `gone`
|
|
45
|
+
* take, so moving it onto a shifted clock would lose them
|
|
46
|
+
* ([LAW:one-source-of-truth] for when it arrives).
|
|
47
|
+
*
|
|
48
|
+
* [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. The host
|
|
49
|
+
* time is an argument everywhere, and a timeline is built from one the host
|
|
50
|
+
* hands in, so the same host time maps to the same animation time at one
|
|
51
|
+
* sample a second or sixty.
|
|
52
|
+
*/
|
|
53
|
+
import { Animation, type Timed } from "./animation.js";
|
|
54
|
+
/**
|
|
55
|
+
* A map from host time to animation time. Playing, it is `time` at `origin`
|
|
56
|
+
* moving `rate` animation seconds a host second; paused, it is `time` at
|
|
57
|
+
* every host time, with the rate it resumes at.
|
|
58
|
+
*/
|
|
59
|
+
export type Timeline = {
|
|
60
|
+
readonly playing: true;
|
|
61
|
+
/** The host time, in seconds, at which the animation time is `time`. */
|
|
62
|
+
readonly origin: number;
|
|
63
|
+
/** The animation time, in seconds, at `origin`. */
|
|
64
|
+
readonly time: number;
|
|
65
|
+
/** Animation seconds per host second; negative runs the animation backwards. Never zero: a held timeline is paused. */
|
|
66
|
+
readonly rate: number;
|
|
67
|
+
} | {
|
|
68
|
+
readonly playing: false;
|
|
69
|
+
/** The animation time, in seconds, held at every host time. */
|
|
70
|
+
readonly time: number;
|
|
71
|
+
/** The rate the timeline resumes at. */
|
|
72
|
+
readonly rate: number;
|
|
73
|
+
};
|
|
74
|
+
export declare const Timeline: {
|
|
75
|
+
/** Playing from host time `origin`, at which the animation time is 0, at `rate` animation seconds a host second. */
|
|
76
|
+
play(origin: number, rate?: number): Timeline;
|
|
77
|
+
/** The animation time at host time `host`. */
|
|
78
|
+
at(timeline: Timeline, host: number): number;
|
|
79
|
+
/** `timeline` held at the animation time it maps `host` to. Pausing a paused timeline holds the same time. */
|
|
80
|
+
pause(timeline: Timeline, host: number): Timeline;
|
|
81
|
+
/** `timeline` playing from `host` at the animation time it maps `host` to, at its rate. Resuming a playing timeline is the same map. */
|
|
82
|
+
resume(timeline: Timeline, host: number): Timeline;
|
|
83
|
+
/** `timeline` with the animation at `time` at host time `host`, playing or held as it was. */
|
|
84
|
+
seek(timeline: Timeline, host: number, time: number): Timeline;
|
|
85
|
+
/** `timeline` at `rate` from host time `host` on, with no jump: the animation time at `host` is what it was. */
|
|
86
|
+
withRate(timeline: Timeline, host: number, rate: number): Timeline;
|
|
87
|
+
/**
|
|
88
|
+
* `animation` on host time: its value at the time `timeline` maps each
|
|
89
|
+
* host time to, changing at the host moment the mapped time reaches the
|
|
90
|
+
* animation's own answer — backwards, when it drops under where the value
|
|
91
|
+
* began. Paused, the value never changes and never did. The value is the
|
|
92
|
+
* animation's own, on no clock: placed, it is stamped at the frame's time
|
|
93
|
+
* (`placedRender`, `./animation.ts`) with this animation's answers, so a
|
|
94
|
+
* value's contents follow the frame's clock and not this map.
|
|
95
|
+
* Reduced, it is the animation reduced on the same timeline: what shows
|
|
96
|
+
* still follows the timeline, and only the motion is gone.
|
|
97
|
+
*/
|
|
98
|
+
run<T>(timeline: Timeline, animation: Timed<T>): Animation<T>;
|
|
99
|
+
/**
|
|
100
|
+
* Every one of `animations` at once, each on a timeline playing from its
|
|
101
|
+
* index times `delay` seconds after the earliest, their values in order, on
|
|
102
|
+
* a clock of the stagger's own from 0. It changes when the soonest of them
|
|
103
|
+
* does, on that clock (`Animation.all` of `run`s). A delay of 0 starts them
|
|
104
|
+
* together and a negative one starts the last at 0 and the first last.
|
|
105
|
+
* Reduced, the offsets are the cascade's choreography, which is motion: every
|
|
106
|
+
* child reduced at once, so from 0 everyone has arrived.
|
|
107
|
+
*/
|
|
108
|
+
stagger<T>(animations: readonly Timed<T>[], delay: number): Animation<readonly T[]>;
|
|
109
|
+
};
|