@promptctl/rich-js 0.18.1 → 0.20.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 +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
package/dist/core/markup.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Plugin tags ([LAW:locality-or-seam]): a `MarkupRegistry` lets consumers
|
|
6
6
|
* register tag handlers without forking the parser. When the parser sees
|
|
7
7
|
* `[name attrs...]inner[/name]` and `name` is registered, it parses attrs,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* renders the inner markup as the child `RichText`, and calls the handler —
|
|
9
|
+
* splicing the `RichText` it returns into the output. The
|
|
10
10
|
* built-in style dialect and the plugin dialect are routed by the registry,
|
|
11
11
|
* which is the single trust boundary between them.
|
|
12
12
|
*
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
* (`[bold]a[italic]b[/bold]c[/italic]`) because a style is an annotation and
|
|
15
15
|
* annotations may overlap freely; a plugin tag is a replacement whose handler
|
|
16
16
|
* takes one contiguous `inner`, so an overlapping pair has no slice to hand it
|
|
17
|
-
* and is rejected with a `MarkupSyntaxError`.
|
|
17
|
+
* and is rejected with a `MarkupSyntaxError`. For the same reason a style tag
|
|
18
|
+
* may not open on one side of a plugin tag and close on the other.
|
|
18
19
|
*/
|
|
19
20
|
import { cellLen } from "./cells.js";
|
|
20
21
|
import { Style, StyleSyntaxError } from "./style.js";
|
|
21
|
-
import { RichText,
|
|
22
|
+
import { RichText, stripControlChars } from "./text.js";
|
|
22
23
|
import { emojiReplace } from "./emoji.js";
|
|
23
24
|
// --- Tag ---
|
|
24
25
|
export class Tag {
|
|
@@ -73,8 +74,7 @@ export class MarkupSyntaxError extends MarkupError {
|
|
|
73
74
|
/** 1-based column of `offset` within its line, in UTF-16 code units. */
|
|
74
75
|
column;
|
|
75
76
|
/**
|
|
76
|
-
* The opening tags still open at `offset`, outermost first, as written.
|
|
77
|
-
* overlapping plugin tags, only plugin tags are named.
|
|
77
|
+
* The opening tags still open at `offset`, outermost first, as written.
|
|
78
78
|
*/
|
|
79
79
|
openTags;
|
|
80
80
|
constructor(reason, markup, offset, openTags) {
|
|
@@ -249,193 +249,28 @@ function tokenize(markup) {
|
|
|
249
249
|
function isTag(token) {
|
|
250
250
|
return token.kind === "tag";
|
|
251
251
|
}
|
|
252
|
-
/**
|
|
253
|
-
* The tokens that begin in `[from, to)` of the source. `tokenize` emits them in
|
|
254
|
-
* source order, so this is two binary searches and a slice: a string of many
|
|
255
|
-
* plugin pairs costs what it holds, not pairs × tokens.
|
|
256
|
-
*/
|
|
257
|
-
function within(tokens, from, to) {
|
|
258
|
-
return tokens.slice(firstAtOrAfter(tokens, from), firstAtOrAfter(tokens, to));
|
|
259
|
-
}
|
|
260
|
-
function firstAtOrAfter(tokens, offset) {
|
|
261
|
-
let low = 0;
|
|
262
|
-
let high = tokens.length;
|
|
263
|
-
while (low < high) {
|
|
264
|
-
const mid = (low + high) >>> 1;
|
|
265
|
-
if (tokens[mid].start < offset)
|
|
266
|
-
low = mid + 1;
|
|
267
|
-
else
|
|
268
|
-
high = mid;
|
|
269
|
-
}
|
|
270
|
-
return low;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Parses the built-in style dialect — `[bold red]text[/bold red]` — into a
|
|
274
|
-
* `RichText` with styled spans. Module-private on purpose: `renderMarkup` is
|
|
275
|
-
* this module's one crossing, and it hands every slice here once its plugin
|
|
276
|
-
* pairs are resolved.
|
|
277
|
-
*
|
|
278
|
-
* [LAW:single-enforcer] Exporting this is what let a caller bind to the inner
|
|
279
|
-
* layer, and two of them did — `console.ts` and `prompt.ts` imported it as
|
|
280
|
-
* `render as renderMarkup`, so the import line read identically to the
|
|
281
|
-
* plugin-aware sites and the difference was invisible at every point of use. A
|
|
282
|
-
* tag registered on `globalMarkupRegistry` resolved in a table cell and was
|
|
283
|
-
* silently eaten by `console.print`, the path almost every consumer takes
|
|
284
|
-
* (rich-markup-pcp). Unexported, that drift is a compile error rather than a
|
|
285
|
-
* rule someone has to keep remembering.
|
|
286
|
-
*
|
|
287
|
-
* [LAW:dataflow-not-control-flow] Rendering *without* plugins stays reachable
|
|
288
|
-
* as a value rather than a second name: `renderMarkup(s, { registry: new
|
|
289
|
-
* MarkupRegistry() })`. Two exported functions put that variability in the
|
|
290
|
-
* function names; an empty registry puts it in the data, where the one
|
|
291
|
-
* boundary admits both.
|
|
292
|
-
*/
|
|
293
|
-
function render(parts, origin, baseStyle, options) {
|
|
294
|
-
const doEmoji = options?.emoji !== false;
|
|
295
|
-
// Build plain text and track spans
|
|
296
|
-
let plainText = "";
|
|
297
|
-
const spans = [];
|
|
298
|
-
const spliced = [];
|
|
299
|
-
const openStack = [];
|
|
300
|
-
const unparsable = (reason, tag) => new MarkupSyntaxError(reason, origin.source, tag.start, [
|
|
301
|
-
...origin.enclosing,
|
|
302
|
-
...openStack.map((opened) => opened.tag),
|
|
303
|
-
]);
|
|
304
|
-
// [LAW:dataflow-not-control-flow] One stack across every part, so a style
|
|
305
|
-
// tag opened before a plugin pair is still open after it and its span covers
|
|
306
|
-
// the handler's output like any other run of text (rich-markup-cg6).
|
|
307
|
-
for (const part of parts) {
|
|
308
|
-
if (part.kind === "rendered") {
|
|
309
|
-
spliced.push({ at: plainText.length, text: part.text });
|
|
310
|
-
plainText += part.text.plain;
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
for (const token of part.tokens) {
|
|
314
|
-
if (!isTag(token)) {
|
|
315
|
-
// A `\[` in front of a bracket no tag starts at is unescaped here, per
|
|
316
|
-
// text token, as the reference does; a tag's own escape was settled by
|
|
317
|
-
// `tokenize` against its backslash run.
|
|
318
|
-
const text = token.text.replace(/\\\[/g, "[");
|
|
319
|
-
// Text reaches `plainText` as `RichText` will hold it, so every offset
|
|
320
|
-
// counted along the way — a span, a splice point — indexes the text it
|
|
321
|
-
// lands on. Stripped only by the constructor, a control character
|
|
322
|
-
// shifted every style after it one cell right.
|
|
323
|
-
plainText += stripControlChars(doEmoji ? emojiReplace(text) : text);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const tag = token;
|
|
327
|
-
if (tag.isImplicitClose) {
|
|
328
|
-
// [/] — close the most recent open tag
|
|
329
|
-
if (openStack.length === 0) {
|
|
330
|
-
throw unparsable(`Closing tag ${tag.fullMatch} has no open tag to close`, tag);
|
|
331
|
-
}
|
|
332
|
-
const opened = openStack.pop();
|
|
333
|
-
spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
|
|
334
|
-
}
|
|
335
|
-
else if (tag.isClosing) {
|
|
336
|
-
// [/style] — find and close matching open tag
|
|
337
|
-
const idx = findLastOpen(openStack, tag.styleName);
|
|
338
|
-
if (idx === -1) {
|
|
339
|
-
throw unparsable(`Closing tag ${tag.fullMatch} doesn't match any open tag`, tag);
|
|
340
|
-
}
|
|
341
|
-
const opened = openStack[idx];
|
|
342
|
-
spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
|
|
343
|
-
openStack.splice(idx, 1);
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
// Opening tag
|
|
347
|
-
openStack.push({
|
|
348
|
-
tag: tag.fullMatch,
|
|
349
|
-
styleName: tag.styleName,
|
|
350
|
-
parameters: tag.parameters,
|
|
351
|
-
textStart: plainText.length,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// Auto-close what is still open, innermost first. The reference pops its
|
|
357
|
-
// stack here (`while style_stack: start, tag = style_stack.pop()`), and
|
|
358
|
-
// popping is what keeps every span in this list in the order its tag closed
|
|
359
|
-
// — the one order the sort below is defined against. Walking the stack
|
|
360
|
-
// forwards instead put the outermost unclosed tag in first, and that lone
|
|
361
|
-
// disagreement was invisible while nothing sorted: it cancelled the missing
|
|
362
|
-
// sort, so `[red][blue]x` was the one nesting this port already got right.
|
|
363
|
-
while (openStack.length > 0) {
|
|
364
|
-
const opened = openStack.pop();
|
|
365
|
-
spans.push(new Span(opened.textStart, plainText.length, openTagStyle(opened)));
|
|
366
|
-
}
|
|
367
|
-
// [LAW:one-source-of-truth] The paint order is the reference's, expressed
|
|
368
|
-
// rather than re-derived: `sorted(spans[::-1], key=attrgetter("start"))` in
|
|
369
|
-
// `rich/markup.py`. `RichText` applies spans in list order and each one adds
|
|
370
|
-
// over the last, so this list's order *is* which style wins where two
|
|
371
|
-
// overlap; sorting by start lays the outer span down first and lets the
|
|
372
|
-
// inner one repaint the run it sits in.
|
|
373
|
-
//
|
|
374
|
-
// The reversal is the whole of the tie-break, not decoration.
|
|
375
|
-
// `[red][blue]x[/blue][/red]` yields two spans that both start at 0, and a
|
|
376
|
-
// stable sort leaves those in the order they arrived — closing order,
|
|
377
|
-
// innermost first — which is the outer colour winning. Reversed first, the
|
|
378
|
-
// outer arrives first and the inner repaints it (rich-markup-krk).
|
|
379
|
-
spans.reverse();
|
|
380
|
-
spans.sort((a, b) => a.start - b.start);
|
|
381
|
-
const result = new RichText(plainText);
|
|
382
|
-
if (baseStyle)
|
|
383
|
-
result.stylize(baseStyle);
|
|
384
|
-
// Apply link parameters as link styles
|
|
385
|
-
for (const span of spans) {
|
|
386
|
-
const style = typeof span.style === "string" ? span.style : span.style.toString();
|
|
387
|
-
// Check if this is a link tag (name=url pattern was parsed)
|
|
388
|
-
const linkMatch = /^link\s+(.+)$/.exec(style);
|
|
389
|
-
if (linkMatch) {
|
|
390
|
-
result.stylize(new Style({ link: linkMatch[1] }), span.start, span.end);
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
result.stylize(style, span.start, span.end);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// A handler's own styles go down last — its base style, then its spans.
|
|
397
|
-
// Every style span that reaches into a handler's output encloses all of it —
|
|
398
|
-
// no tag of this walk sits inside a plugin pair — so each of these is the
|
|
399
|
-
// inner span, and inner repaints outer exactly as the sort above has it for
|
|
400
|
-
// tags.
|
|
401
|
-
for (const { at, text } of spliced) {
|
|
402
|
-
result.stylize(text.style, at, at + text.length);
|
|
403
|
-
for (const span of text.spans)
|
|
404
|
-
result.stylize(span.style, at + span.start, at + span.end);
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
407
|
-
}
|
|
408
252
|
/**
|
|
409
253
|
* The style string an open tag closes into — `name`, or `name parameters` when
|
|
410
254
|
* the tag carried an `=`.
|
|
411
255
|
*
|
|
412
|
-
* [LAW:one-source-of-truth] One answer for the
|
|
413
|
-
*
|
|
414
|
-
* matches `[/red on blue]` back to
|
|
415
|
-
*
|
|
416
|
-
* something none of the
|
|
256
|
+
* [LAW:one-source-of-truth] One answer for the two sites that ask it: the span
|
|
257
|
+
* a tag closes into, whichever of `[/name]`, `[/]`, a plugin pair's end or the
|
|
258
|
+
* end of input closed it, and the search that matches `[/red on blue]` back to
|
|
259
|
+
* `[red on blue]`. It was once written out at each closing path, which is how
|
|
260
|
+
* those paths could — and did — drift apart on something none of the copies
|
|
261
|
+
* was about.
|
|
417
262
|
*/
|
|
418
263
|
function openTagStyle(opened) {
|
|
419
264
|
return opened.parameters !== undefined
|
|
420
265
|
? `${opened.styleName} ${opened.parameters}`
|
|
421
266
|
: opened.styleName;
|
|
422
267
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Whether `[/name]` closes `opened`.
|
|
425
|
-
*
|
|
426
|
-
* [LAW:one-source-of-truth] Two walks ask this: the built-in parser closing a
|
|
427
|
-
* style, and the plugin pass keeping the same stack so it knows what `[/]`
|
|
428
|
-
* refers to. If they disagreed on which open tag `[/red on blue]` takes off
|
|
429
|
-
* the stack, they would disagree about what the next `[/]` closes.
|
|
430
|
-
*/
|
|
268
|
+
/** Whether `[/name]` names the style tag `opened`. */
|
|
431
269
|
function closesByName(opened, name) {
|
|
432
270
|
// Handle "on" in style names for closing: [/red on blue] should match [red on blue]
|
|
433
271
|
const normalized = name.trim();
|
|
434
272
|
return openTagStyle(opened) === normalized || opened.styleName === normalized;
|
|
435
273
|
}
|
|
436
|
-
function findLastOpen(stack, name) {
|
|
437
|
-
return findLastIndex(stack, (entry) => closesByName(entry, name));
|
|
438
|
-
}
|
|
439
274
|
function findLastIndex(items, match) {
|
|
440
275
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
441
276
|
if (match(items[i]))
|
|
@@ -575,157 +410,245 @@ function parsePluginAttrs(after) {
|
|
|
575
410
|
* Plugin-aware markup render. Always returns a `RichText`. Built-in style
|
|
576
411
|
* tags become spans; registered plugin tags are resolved by their handler,
|
|
577
412
|
* whose returned `RichText` stands in the output where its pair stood, under
|
|
578
|
-
* any style tag that encloses the pair.
|
|
579
|
-
* of each plugin pair, and every recursion node returns a `RichText`.
|
|
413
|
+
* any style tag that encloses the pair.
|
|
580
414
|
*/
|
|
581
415
|
export function renderMarkup(markup, options) {
|
|
582
|
-
return renderSlice(tokenize(markup), { source: markup, enclosing: [] }, options);
|
|
583
|
-
}
|
|
584
|
-
function renderSlice(tokens, origin, options) {
|
|
585
416
|
const registry = options?.registry ?? globalMarkupRegistry;
|
|
586
|
-
|
|
587
|
-
// splice walk can just iterate top-level pairs in source order with no
|
|
588
|
-
// nested-state book-keeping.
|
|
589
|
-
const { annotated, topLevel: tagPairs } = pairPluginTags(tokens.filter(isTag), registry, origin);
|
|
590
|
-
// [LAW:one-type-per-behavior] Each top-level plugin pair is resolved by its
|
|
591
|
-
// handler, and what it returns takes the pair's place in one built-in walk
|
|
592
|
-
// over the whole slice. Rendering the text between pairs as separate parses
|
|
593
|
-
// is what closed every style tag at a plugin boundary (rich-markup-cg6).
|
|
594
|
-
const parts = [];
|
|
595
|
-
let cursor = 0;
|
|
596
|
-
for (const [openIdx, closeIdx] of tagPairs) {
|
|
597
|
-
const open = annotated[openIdx];
|
|
598
|
-
const close = annotated[closeIdx];
|
|
599
|
-
parts.push({ kind: "markup", tokens: within(tokens, cursor, open.start) });
|
|
600
|
-
const innerRaw = origin.source.slice(open.end, close.start);
|
|
601
|
-
// The base style is painted once, under the whole output, by the top of
|
|
602
|
-
// this recursion. Painted into `children` as well, it came back spliced in
|
|
603
|
-
// over every tag enclosing the pair.
|
|
604
|
-
const innerRichText = renderSlice(within(tokens, open.end, close.start), { ...origin, enclosing: [...origin.enclosing, open.fullMatch] }, { ...options, baseStyle: undefined });
|
|
605
|
-
const handler = registry.get(open.pluginName);
|
|
606
|
-
parts.push({ kind: "rendered", text: handler({ attrs: open.attrs, children: innerRichText, raw: innerRaw }) });
|
|
607
|
-
cursor = close.end;
|
|
608
|
-
}
|
|
609
|
-
parts.push({ kind: "markup", tokens: within(tokens, cursor, Infinity) });
|
|
610
|
-
return render(parts, origin, options?.baseStyle, { emoji: options?.emoji !== false });
|
|
417
|
+
return compose(walk(tokenize(markup), markup, registry, options?.emoji !== false), options?.baseStyle);
|
|
611
418
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
419
|
+
/**
|
|
420
|
+
* A string as the text it draws, before any highlighting: read as markup
|
|
421
|
+
* unless `markup` is false, its emoji codes replaced either way, as Rich's
|
|
422
|
+
* `render_str` reads one, its `end` the default line end Rich's leaves it.
|
|
423
|
+
* [LAW:single-enforcer]
|
|
424
|
+
*/
|
|
425
|
+
export function readStr(source, markup) {
|
|
426
|
+
return markup ? renderMarkup(source) : new RichText(emojiReplace(source));
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The highlighter a string is drawn with under `settings`: its `highlighter`
|
|
430
|
+
* while `highlight` is not false — Rich's `render_str`, which highlights with
|
|
431
|
+
* `console.highlighter` when `highlight` is true or left to the console.
|
|
432
|
+
* [LAW:single-enforcer]
|
|
433
|
+
*/
|
|
434
|
+
export function activeHighlighter(settings) {
|
|
435
|
+
return settings.highlight === false ? undefined : settings.highlighter;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* A string as the text it draws, the way Rich's `render_str` makes one: read
|
|
439
|
+
* by `readStr` under `markup`, then highlighted by `activeHighlighter`.
|
|
440
|
+
* `print` and every renderable that is handed a string both draw it this way,
|
|
441
|
+
* so a console's settings mean the same thing wherever the string lands.
|
|
442
|
+
*/
|
|
443
|
+
export function renderStr(source, settings) {
|
|
444
|
+
const text = readStr(source, settings.markup !== false);
|
|
445
|
+
activeHighlighter(settings)?.highlight(text);
|
|
446
|
+
return text;
|
|
447
|
+
}
|
|
448
|
+
function markOf(out) {
|
|
449
|
+
return { at: out.length, chunks: out.chunks.length, spans: out.spans.length, spliced: out.spliced.length };
|
|
450
|
+
}
|
|
451
|
+
function write(out, text) {
|
|
452
|
+
out.chunks.push(text);
|
|
453
|
+
out.length += text.length;
|
|
454
|
+
}
|
|
455
|
+
/** Everything written since `mark`, taken off `out` and positioned from 0. */
|
|
456
|
+
function takeFrom(out, mark) {
|
|
457
|
+
out.length = mark.at;
|
|
458
|
+
return {
|
|
459
|
+
plain: out.chunks.splice(mark.chunks).join(""),
|
|
460
|
+
spans: out.spans.splice(mark.spans).map(({ start, end, style }) => ({ start: start - mark.at, end: end - mark.at, style })),
|
|
461
|
+
spliced: out.spliced.splice(mark.spliced).map(({ at, text }) => ({ at: at - mark.at, text })),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function isPlugin(opened) {
|
|
465
|
+
return opened.plugin !== undefined;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Reads a whole markup string's tokens into one frame: every style tag a
|
|
469
|
+
* span, every plugin pair resolved by its handler.
|
|
470
|
+
*
|
|
471
|
+
* [LAW:one-source-of-truth] One stack of open tags and one rule for what each
|
|
472
|
+
* closing tag closes — `namedBy` — for style tags and plugin tags alike. Plugin
|
|
473
|
+
* pairs used to be found by a walk of their own, ahead of the one that built
|
|
474
|
+
* spans, and each kept its own stack; the two had to agree on every closing
|
|
475
|
+
* rule, and review found three places they did not (rich-markup-pyq5). One
|
|
476
|
+
* walk also rejects in string order, since every tag is judged when it is
|
|
477
|
+
* reached: a crossing found by the pairing walk used to hide a stray closer
|
|
478
|
+
* before it.
|
|
479
|
+
*
|
|
480
|
+
* Module-private on purpose, as `compose` is: `renderMarkup` is this module's
|
|
481
|
+
* one crossing. [LAW:single-enforcer] The built-in parser was once exported,
|
|
482
|
+
* and two callers bound to it under the name `renderMarkup`, so a tag on
|
|
483
|
+
* `globalMarkupRegistry` resolved in a table cell and was silently eaten by
|
|
484
|
+
* `console.print` (rich-markup-pcp).
|
|
485
|
+
*
|
|
486
|
+
* A plugin tag's content is everything written after it, because the walk
|
|
487
|
+
* cannot know yet whether the tag will pair. When it pairs, that tail is taken
|
|
488
|
+
* off the output as the handler's `children`, and the handler's output is
|
|
489
|
+
* written in its place. When it never does — an outer pair's end or the end
|
|
490
|
+
* of the string closes it — nothing moves: the tag is a style span over its
|
|
491
|
+
* content, like any tag naming no style, and costs what one does.
|
|
492
|
+
*/
|
|
493
|
+
function walk(tokens, source, registry, emoji) {
|
|
494
|
+
const out = { chunks: [], length: 0, spans: [], spliced: [] };
|
|
495
|
+
const start = markOf(out);
|
|
627
496
|
const stack = [];
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
497
|
+
// Every tag opened so far, in source order, for a rejection to look back on.
|
|
498
|
+
const opened = [];
|
|
499
|
+
const openAt = (offset) => opened.filter((o) => o.tag.start < offset && (o.closed?.at ?? Infinity) >= offset).map((o) => o.tag.fullMatch);
|
|
500
|
+
const unparsable = (reason, offset) => new MarkupSyntaxError(reason, source, offset, openAt(offset));
|
|
501
|
+
const crossing = (style, closer, pair) => unparsable(`Closing tag ${closer.fullMatch} closes ${style.tag.fullMatch} ` +
|
|
502
|
+
`across the boundary of plugin tag ${pair.tag.fullMatch}: ` +
|
|
503
|
+
`a style tag must open and close on the same side of a plugin tag, ` +
|
|
504
|
+
`because a handler replaces the text between a plugin tag and its closer.`, closer.start);
|
|
505
|
+
// [LAW:no-silent-failure] A closing tag that closes nothing reaches for the
|
|
506
|
+
// last tag before it that it names. If a pair's end closed that tag, the
|
|
507
|
+
// closer reaches across the pair, and saying so beats "doesn't match".
|
|
508
|
+
const stray = (closer, plugin) => {
|
|
509
|
+
const named = opened.filter(namedBy(closer, plugin)).at(-1);
|
|
510
|
+
const closed = named?.closed;
|
|
511
|
+
const pair = closed?.byPair;
|
|
512
|
+
if (named === undefined || closed === undefined || pair === undefined) {
|
|
513
|
+
return unparsable(closer.isImplicitClose
|
|
514
|
+
? `Closing tag ${closer.fullMatch} has no open tag to close`
|
|
515
|
+
: `Closing tag ${closer.fullMatch} doesn't match any open tag`, closer.start);
|
|
516
|
+
}
|
|
517
|
+
if (!pairs(closer, plugin, named))
|
|
518
|
+
return crossing(named, closer, pair);
|
|
519
|
+
// Overlap is unrepresentable rather than unimplemented: a handler receives
|
|
520
|
+
// `children` as one contiguous slice, so a region straddling another pair's
|
|
521
|
+
// closing boundary has nothing to hand it. A style span may cross another
|
|
522
|
+
// style span because it annotates; a plugin pair may not because it
|
|
523
|
+
// replaces. The caret goes where nesting broke: the outer pair's end.
|
|
524
|
+
return unparsable(`Plugin tag [${named.plugin.name}] overlaps [${pair.plugin.name}]: plugin tags must nest, ` +
|
|
525
|
+
`because a handler receives one contiguous slice. ` +
|
|
526
|
+
`Close [/${named.plugin.name}] before [/${pair.plugin.name}].`, closed.at);
|
|
645
527
|
};
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
for (
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
528
|
+
// Closes a tag as a style span over what was written since it opened. A
|
|
529
|
+
// plugin tag closed this way never paired.
|
|
530
|
+
const settle = (tag, at, byPair) => {
|
|
531
|
+
tag.closed = at === undefined ? undefined : { at, byPair };
|
|
532
|
+
out.spans.push({ start: tag.mark.at, end: out.length, style: openTagStyle(tag.tag) });
|
|
533
|
+
};
|
|
534
|
+
for (const token of tokens) {
|
|
535
|
+
if (!isTag(token)) {
|
|
536
|
+
// A `\[` in front of a bracket no tag starts at is unescaped here, per
|
|
537
|
+
// text token, as the reference does; a tag's own escape was settled by
|
|
538
|
+
// `tokenize` against its backslash run.
|
|
539
|
+
const text = token.text.replace(/\\\[/g, "[");
|
|
540
|
+
// Text reaches the output as `RichText` will hold it, so every offset
|
|
541
|
+
// counted along the way — a span, a splice point — indexes the text it
|
|
542
|
+
// lands on. Stripped only by the constructor, a control character
|
|
543
|
+
// shifted every style after it one cell right.
|
|
544
|
+
write(out, stripControlChars(emoji ? emojiReplace(text) : text));
|
|
656
545
|
continue;
|
|
657
546
|
}
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
// otherwise the built-in parser rejects it, with the location it reports.
|
|
665
|
-
let target = i - 1;
|
|
666
|
-
while (target >= 0 && !(isOpener(annotated[target]) && closes(t)(target)))
|
|
667
|
-
target--;
|
|
668
|
-
const pairOpen = closedByPair.get(target);
|
|
669
|
-
if (pairOpen === undefined)
|
|
670
|
-
continue;
|
|
671
|
-
if (t.pluginName === undefined)
|
|
672
|
-
throw crossing(target, i, pairOpen);
|
|
673
|
-
// Overlap is unrepresentable rather than unimplemented: a handler receives
|
|
674
|
-
// `children` as one contiguous slice, so a region straddling another
|
|
675
|
-
// pair's closing boundary has nothing to hand it. A style span may cross
|
|
676
|
-
// another style span because it annotates; a plugin pair may not because
|
|
677
|
-
// it replaces. The caret goes where nesting broke: the outer pair's end.
|
|
678
|
-
const inner = annotated[target];
|
|
679
|
-
const outer = annotated[pairOpen];
|
|
680
|
-
const caret = closedAt.get(pairOpen);
|
|
681
|
-
throw new MarkupSyntaxError(`Plugin tag [${inner.pluginName}] overlaps [${outer.pluginName}]: plugin tags must nest, ` +
|
|
682
|
-
`because a handler receives one contiguous slice. ` +
|
|
683
|
-
`Close [/${inner.pluginName}] before [/${outer.pluginName}].`, origin.source, annotated[caret].start, [...origin.enclosing, ...openAt(caret)]);
|
|
547
|
+
const plugin = pluginOf(token, registry);
|
|
548
|
+
if (!token.isClosing && !token.isImplicitClose) {
|
|
549
|
+
const entry = { tag: token, mark: markOf(out), plugin, closed: undefined };
|
|
550
|
+
stack.push(entry);
|
|
551
|
+
opened.push(entry);
|
|
552
|
+
continue;
|
|
684
553
|
}
|
|
685
|
-
const
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
554
|
+
const j = findLastIndex(stack, namedBy(token, plugin));
|
|
555
|
+
if (j === -1)
|
|
556
|
+
throw stray(token, plugin);
|
|
557
|
+
const target = stack[j];
|
|
558
|
+
if (!pairs(token, plugin, target)) {
|
|
559
|
+
// A closer that ends no pair may pass over style tags, which overlap
|
|
560
|
+
// freely, but not over a plugin tag: the handler replaces what is
|
|
561
|
+
// inside it.
|
|
562
|
+
const boundary = findLastIndex(stack, isPlugin);
|
|
563
|
+
if (boundary > j)
|
|
564
|
+
throw crossing(target, token, stack[boundary]);
|
|
691
565
|
stack.splice(j, 1);
|
|
692
|
-
|
|
566
|
+
settle(target, token.start, undefined);
|
|
693
567
|
continue;
|
|
694
568
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
for (const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
569
|
+
// A pair's end closes everything opened inside it, innermost first, so the
|
|
570
|
+
// handler's `children` hold every span its content carries.
|
|
571
|
+
const inside = stack.splice(j).slice(1);
|
|
572
|
+
for (const tag of inside.reverse())
|
|
573
|
+
settle(tag, token.start, target);
|
|
574
|
+
target.closed = { at: token.start, byPair: undefined };
|
|
575
|
+
const { handler, attrs } = target.plugin;
|
|
576
|
+
const children = compose(takeFrom(out, target.mark));
|
|
577
|
+
const text = handler({ attrs, children, raw: source.slice(target.tag.end, token.start) });
|
|
578
|
+
out.spliced.push({ at: out.length, text });
|
|
579
|
+
write(out, text.plain);
|
|
580
|
+
}
|
|
581
|
+
// Close what is still open, innermost first. The reference pops its stack
|
|
582
|
+
// here (`while style_stack: start, tag = style_stack.pop()`), and popping is
|
|
583
|
+
// what keeps every span in the order its tag closed — the one order
|
|
584
|
+
// `compose`'s sort is defined against.
|
|
585
|
+
while (stack.length > 0)
|
|
586
|
+
settle(stack.pop(), undefined, undefined);
|
|
587
|
+
return takeFrom(out, start);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Which open tags a closing tag names: `[/]` every one, and `[/name]` a tag
|
|
591
|
+
* whose style it names or a plugin tag of the plugin it names.
|
|
592
|
+
*/
|
|
593
|
+
function namedBy(closer, plugin) {
|
|
594
|
+
if (closer.isImplicitClose)
|
|
595
|
+
return () => true;
|
|
596
|
+
return (opened) => closesByName(opened.tag, closer.styleName) || pairs(closer, plugin, opened);
|
|
597
|
+
}
|
|
598
|
+
/** Whether `closer` ends a plugin pair at `opened`: `[/]`, or its plugin's own name. */
|
|
599
|
+
function pairs(closer, plugin, opened) {
|
|
600
|
+
return opened.plugin !== undefined && (closer.isImplicitClose || opened.plugin.name === plugin?.name);
|
|
601
|
+
}
|
|
602
|
+
/** A frame as the `RichText` it draws, `baseStyle` beneath all of it. */
|
|
603
|
+
function compose(frame, baseStyle) {
|
|
604
|
+
// [LAW:one-source-of-truth] The paint order is the reference's, expressed
|
|
605
|
+
// rather than re-derived: `sorted(spans[::-1], key=attrgetter("start"))` in
|
|
606
|
+
// `rich/markup.py`. `RichText` applies spans in list order and each one adds
|
|
607
|
+
// over the last, so this list's order *is* which style wins where two
|
|
608
|
+
// overlap; sorting by start lays the outer span down first and lets the
|
|
609
|
+
// inner one repaint the run it sits in.
|
|
610
|
+
//
|
|
611
|
+
// The reversal is the whole of the tie-break, not decoration.
|
|
612
|
+
// `[red][blue]x[/blue][/red]` yields two spans that both start at 0, and a
|
|
613
|
+
// stable sort leaves those in the order they arrived — closing order,
|
|
614
|
+
// innermost first — which is the outer colour winning. Reversed first, the
|
|
615
|
+
// outer arrives first and the inner repaints it (rich-markup-krk).
|
|
616
|
+
const spans = [...frame.spans].reverse().sort((a, b) => a.start - b.start);
|
|
617
|
+
const result = new RichText(frame.plain);
|
|
618
|
+
if (baseStyle)
|
|
619
|
+
result.stylize(baseStyle);
|
|
620
|
+
// Apply link parameters as link styles
|
|
621
|
+
for (const { start, end, style } of spans) {
|
|
622
|
+
// Check if this is a link tag (name=url pattern was parsed)
|
|
623
|
+
const linkMatch = /^link\s+(.+)$/.exec(style);
|
|
624
|
+
if (linkMatch) {
|
|
625
|
+
result.stylize(new Style({ link: linkMatch[1] }), start, end);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
result.stylize(style, start, end);
|
|
702
629
|
}
|
|
703
|
-
pairs.set(openIdx, i);
|
|
704
630
|
}
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
outerClose = pairs.get(openIdx);
|
|
715
|
-
topLevel.set(openIdx, outerClose);
|
|
631
|
+
// A handler's own styles go down last — its base style, then its spans.
|
|
632
|
+
// Every style span that reaches into a handler's output encloses all of it —
|
|
633
|
+
// no style tag may cross a plugin pair's boundary — so each of these is the
|
|
634
|
+
// inner span, and inner repaints outer exactly as the sort above has it for
|
|
635
|
+
// tags.
|
|
636
|
+
for (const { at, text } of frame.spliced) {
|
|
637
|
+
result.stylize(text.style, at, at + text.length);
|
|
638
|
+
for (const span of text.spans)
|
|
639
|
+
result.stylize(span.style, at + span.start, at + span.end);
|
|
716
640
|
}
|
|
717
|
-
return
|
|
641
|
+
return result;
|
|
718
642
|
}
|
|
719
|
-
function
|
|
720
|
-
// The
|
|
643
|
+
function pluginOf(tag, registry) {
|
|
644
|
+
// The tokenizer splits at the first `=`, so tag.styleName for
|
|
721
645
|
// `[click verb=foo]` is "click verb". Re-read the tag text for a name that
|
|
722
646
|
// actually addresses a plugin.
|
|
723
647
|
const inner = tag.fullMatch.slice(1, -1).replace(/^\//, "");
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
if (name === undefined ||
|
|
727
|
-
return
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
return { ...tag, pluginName: name, attrs };
|
|
648
|
+
const name = PLUGIN_TAG_HEAD.exec(inner)?.[1];
|
|
649
|
+
const handler = name === undefined ? undefined : registry.get(name);
|
|
650
|
+
if (name === undefined || handler === undefined)
|
|
651
|
+
return undefined;
|
|
652
|
+
const attrs = tag.isClosing ? {} : parsePluginAttrs(inner.slice(name.length));
|
|
653
|
+
return { name, handler, attrs };
|
|
731
654
|
}
|
package/dist/core/measure.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Measurement — min/max cell width calculation for renderables.
|
|
3
3
|
*/
|
|
4
|
-
import { type Measurable, type RenderOptions } from "./protocol.js";
|
|
4
|
+
import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
|
|
5
5
|
export declare class Measurement {
|
|
6
6
|
readonly minimum: number;
|
|
7
7
|
readonly maximum: number;
|
|
@@ -22,6 +22,6 @@ export declare class Measurement {
|
|
|
22
22
|
withMaximum(width: number): Measurement;
|
|
23
23
|
withMinimum(width: number): Measurement;
|
|
24
24
|
clamp(minWidth: number, maxWidth: number): Measurement;
|
|
25
|
-
static get(options: RenderOptions,
|
|
25
|
+
static get(options: RenderOptions, renderable: Renderable | Measurable): Measurement;
|
|
26
26
|
}
|
|
27
|
-
export declare function measureRenderables(options: RenderOptions,
|
|
27
|
+
export declare function measureRenderables(options: RenderOptions, renderables: readonly (Renderable | Measurable)[]): Measurement;
|