@promptctl/rich-js 0.17.0 → 0.18.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.
- package/README.md +13 -26
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +0 -1
- package/dist/core/box.js +6 -18
- package/dist/core/cells.d.ts +16 -5
- package/dist/core/cells.js +31 -10
- package/dist/core/color.d.ts +4 -8
- package/dist/core/color.js +28 -19
- package/dist/core/console.d.ts +13 -4
- package/dist/core/console.js +91 -36
- package/dist/core/export-html.d.ts +40 -5
- package/dist/core/export-html.js +111 -17
- package/dist/core/markup.d.ts +27 -5
- package/dist/core/markup.js +282 -170
- package/dist/core/measure.d.ts +1 -1
- package/dist/core/measure.js +2 -1
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +3 -0
- package/dist/core/pretty.js +5 -1
- package/dist/core/protocol.d.ts +136 -6
- package/dist/core/protocol.js +64 -2
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +2 -2
- package/dist/core/segment.d.ts +23 -4
- package/dist/core/segment.js +75 -16
- package/dist/core/spinnerData.d.ts +2 -0
- package/dist/core/spinnerData.js +3 -1
- package/dist/core/strip.d.ts +3 -7
- package/dist/core/strip.js +32 -35
- package/dist/core/style.d.ts +25 -0
- package/dist/core/style.js +61 -89
- package/dist/core/text.d.ts +16 -15
- package/dist/core/text.js +62 -76
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +4 -2
- package/dist/host/index.js +2 -1
- package/dist/host/terminal-host.d.ts +43 -7
- package/dist/host/terminal-host.js +57 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/node/terminal-host.d.ts +31 -2
- package/dist/node/terminal-host.js +69 -2
- package/dist/node/traceback.d.ts +16 -6
- package/dist/node/traceback.js +30 -8
- package/dist/renderables/align.d.ts +2 -2
- package/dist/renderables/align.js +6 -19
- package/dist/renderables/columns.js +4 -20
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/group.js +3 -1
- package/dist/renderables/layout.js +31 -34
- package/dist/renderables/live.d.ts +6 -3
- package/dist/renderables/live.js +89 -45
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.js +6 -4
- package/dist/renderables/padding.js +3 -2
- package/dist/renderables/panel.js +26 -41
- package/dist/renderables/progress.js +5 -8
- package/dist/renderables/progressBar.js +6 -5
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +9 -9
- package/dist/renderables/spinner.d.ts +5 -3
- package/dist/renderables/spinner.js +14 -8
- package/dist/renderables/syntax.d.ts +1 -1
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +26 -14
- package/dist/renderables/table.js +239 -141
- package/dist/renderables/traceback.js +8 -9
- package/dist/renderables/tree.d.ts +17 -3
- package/dist/renderables/tree.js +62 -71
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.js +11 -13
- package/dist/template-bindings/index.js +4 -2
- package/dist/template-bindings/palette-funcs.js +6 -16
- package/dist/template-bindings/style-funcs.js +4 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.js +19 -0
- package/dist/themes/data/atom-one-light.js +19 -0
- package/dist/themes/data/catppuccin-frappe.js +19 -0
- package/dist/themes/data/catppuccin-latte.js +19 -0
- package/dist/themes/data/catppuccin-macchiato.js +19 -0
- package/dist/themes/data/catppuccin-mocha.js +19 -0
- package/dist/themes/data/cyberpunk.js +2 -0
- package/dist/themes/data/default.js +2 -0
- package/dist/themes/data/dracula.js +19 -0
- package/dist/themes/data/flexoki.js +19 -0
- package/dist/themes/data/gruvbox.js +19 -0
- package/dist/themes/data/monokai.js +19 -0
- package/dist/themes/data/nord.js +19 -0
- package/dist/themes/data/rose-pine-dawn.js +19 -0
- package/dist/themes/data/rose-pine-moon.js +19 -0
- package/dist/themes/data/rose-pine.js +19 -0
- package/dist/themes/data/solarized-dark.js +19 -0
- package/dist/themes/data/solarized-light.js +19 -0
- package/dist/themes/data/svg-export.js +19 -0
- package/dist/themes/data/textual-ansi.js +2 -0
- package/dist/themes/data/textual-dark.js +2 -0
- package/dist/themes/data/textual-light.js +2 -0
- package/dist/themes/data/tokyo-night.js +19 -0
- package/dist/themes/data/types.d.ts +16 -0
- package/dist/themes/data/types.js +10 -1
- package/dist/themes/registry.d.ts +3 -2
- package/dist/themes/registry.js +13 -1
- package/dist/themes/terminalThemes.d.ts +2 -7
- package/dist/themes/terminalThemes.js +4 -9
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +4 -2
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +5 -3
- package/dist/widgets/dropdown.d.ts +2 -2
- package/dist/widgets/dropdown.js +13 -16
- package/dist/widgets/event-router.d.ts +21 -28
- package/dist/widgets/event-router.js +80 -73
- package/dist/widgets/focus-manager.d.ts +25 -6
- package/dist/widgets/focus-manager.js +63 -65
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +5 -7
- package/dist/widgets/index.js +3 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/slider.js +7 -7
- package/dist/widgets/static-item.d.ts +9 -15
- package/dist/widgets/static-item.js +9 -15
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +17 -14
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +3 -2
- package/dist/widgets/types.d.ts +14 -35
- package/dist/widgets/types.js +0 -1
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +4 -6
- package/dist/widgets/widget-base.js +22 -30
- package/package.json +12 -4
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
package/dist/core/color.js
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
function hex2(byte) {
|
|
6
6
|
return byte.toString(16).padStart(2, "0");
|
|
7
7
|
}
|
|
8
|
+
// [LAW:one-source-of-truth] The one statement of a colour byte's range: the
|
|
9
|
+
// constructor asserts it and the string parser reports it, from this predicate.
|
|
10
|
+
function isByte(v) {
|
|
11
|
+
return Number.isInteger(v) && v >= 0 && v <= 255;
|
|
12
|
+
}
|
|
8
13
|
function assertChannel(name, v) {
|
|
9
|
-
if (!
|
|
14
|
+
if (!isByte(v)) {
|
|
10
15
|
throw new RangeError(`ColorRgba.${name} must be an integer in [0, 255]; got ${v}`);
|
|
11
16
|
}
|
|
12
17
|
}
|
|
@@ -501,7 +506,8 @@ export class ColorSpec {
|
|
|
501
506
|
case ColorDepth.TRUECOLOR:
|
|
502
507
|
return this.value;
|
|
503
508
|
case ColorDepth.EIGHT_BIT:
|
|
504
|
-
|
|
509
|
+
// Slots 0–15 are the theme's own whatever depth names them, as `fixedValue` says.
|
|
510
|
+
return this.number < 16 ? (theme ?? INTERNAL_DEFAULT_THEME).ansiColors.get(this.number) : EIGHT_BIT_TABLE.get(this.number);
|
|
505
511
|
case ColorDepth.STANDARD: {
|
|
506
512
|
const t = theme ?? INTERNAL_DEFAULT_THEME;
|
|
507
513
|
return t.ansiColors.get(this.number);
|
|
@@ -569,8 +575,8 @@ export class ColorSpec {
|
|
|
569
575
|
// --- Parsing internals ---
|
|
570
576
|
const HEX_RE = /^#([0-9a-f]{6})$/;
|
|
571
577
|
const HEX_RGBA_RE = /^#([0-9a-f]{8})$/;
|
|
572
|
-
const RGB_RE = /^rgb\(\s*(\d
|
|
573
|
-
const COLOR_NUMBER_RE = /^color\((\d
|
|
578
|
+
const RGB_RE = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
|
|
579
|
+
const COLOR_NUMBER_RE = /^color\((\d+)\)$/;
|
|
574
580
|
function parseSingle(key) {
|
|
575
581
|
if (key === "default" || key === "") {
|
|
576
582
|
return ColorSpec.default();
|
|
@@ -594,23 +600,30 @@ function parseSingle(key) {
|
|
|
594
600
|
// rgb()
|
|
595
601
|
const rgbMatch = RGB_RE.exec(key);
|
|
596
602
|
if (rgbMatch) {
|
|
597
|
-
const r =
|
|
598
|
-
const g =
|
|
599
|
-
const b =
|
|
603
|
+
const r = parseByte(key, rgbMatch[1], "red");
|
|
604
|
+
const g = parseByte(key, rgbMatch[2], "green");
|
|
605
|
+
const b = parseByte(key, rgbMatch[3], "blue");
|
|
600
606
|
return new ColorSpec(key, ColorDepth.TRUECOLOR, undefined, new ColorRgba(r, g, b));
|
|
601
607
|
}
|
|
602
608
|
// color(N)
|
|
603
609
|
const numMatch = COLOR_NUMBER_RE.exec(key);
|
|
604
610
|
if (numMatch) {
|
|
605
|
-
const n =
|
|
606
|
-
if (n > 255) {
|
|
607
|
-
throw new ColorParseError(`ColorSpec number ${n} is out of range (0-255)`);
|
|
608
|
-
}
|
|
611
|
+
const n = parseByte(key, numMatch[1], "number");
|
|
609
612
|
const type = n < 16 ? ColorDepth.STANDARD : ColorDepth.EIGHT_BIT;
|
|
610
613
|
return new ColorSpec(key, type, n);
|
|
611
614
|
}
|
|
612
615
|
throw new ColorParseError(`Failed to parse color: "${key}"`);
|
|
613
616
|
}
|
|
617
|
+
// [LAW:single-enforcer] The regexes admit any run of digits and leave the
|
|
618
|
+
// range to this check alone, so every out-of-range byte is a ColorParseError
|
|
619
|
+
// naming the field, never the generic parse failure or the constructor's RangeError.
|
|
620
|
+
function parseByte(key, digits, field) {
|
|
621
|
+
const n = parseInt(digits, 10);
|
|
622
|
+
if (!isByte(n)) {
|
|
623
|
+
throw new ColorParseError(`ColorSpec "${key}": ${field} ${digits} is out of range (0-255)`);
|
|
624
|
+
}
|
|
625
|
+
return n;
|
|
626
|
+
}
|
|
614
627
|
// --- Utility functions ---
|
|
615
628
|
export function parseRgbHex(hex) {
|
|
616
629
|
const r = parseInt(hex.slice(0, 2), 16);
|
|
@@ -633,14 +646,10 @@ export function blendRgb(color1, color2, crossFade = 0.5) {
|
|
|
633
646
|
* A terminal theme — surface/foreground baseline, the ANSI 16/256 LUT, and a
|
|
634
647
|
* semantic palette.
|
|
635
648
|
*
|
|
636
|
-
* **`ansiColors` is the
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* design as Textual itself, which models theming at the truecolor/semantic
|
|
641
|
-
* layer rather than overloading the ANSI 16. Custom callers building bespoke
|
|
642
|
-
* themes that *do* want to retint ANSI 16 are free to pass their own
|
|
643
|
-
* `ColorTable`; that path is supported but not how the built-in presets work.
|
|
649
|
+
* **`ansiColors` is the theme's own sixteen colours**, the shades a terminal
|
|
650
|
+
* showing this theme draws `red`, `blue` and the rest in. `ColorSpec.parse("red")`
|
|
651
|
+
* is ANSI colour 1 under every theme; which red that is, is the theme's to say,
|
|
652
|
+
* as it is in Rich, where each `TerminalTheme` carries its own table.
|
|
644
653
|
*/
|
|
645
654
|
export class TerminalTheme {
|
|
646
655
|
backgroundColor;
|
package/dist/core/console.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { TerminalTheme } from "./color.js";
|
|
|
8
8
|
import { type JSONOptions } from "./json.js";
|
|
9
9
|
import type { Highlighter } from "./highlighter.js";
|
|
10
10
|
import { type RuleOptions } from "../renderables/rule.js";
|
|
11
|
-
import type { RenderOptions, StyleErrorHandler } from "./protocol.js";
|
|
11
|
+
import type { Height, OverflowMethod, RenderOptions, StyleErrorHandler } from "./protocol.js";
|
|
12
12
|
export interface ConsoleSink {
|
|
13
13
|
write(chunk: string | Uint8Array): unknown;
|
|
14
14
|
}
|
|
@@ -41,6 +41,13 @@ export interface ConsoleOptions {
|
|
|
41
41
|
* TERM=dumb: false). `false` with `colorSystem: null` writes plain text.
|
|
42
42
|
*/
|
|
43
43
|
hyperlinks?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The terminal can draw only ASCII: boxes, rules, tree guides and widget
|
|
46
|
+
* marks draw with ASCII characters instead of line-drawing glyphs. A
|
|
47
|
+
* property of the output device, so it is set here once and reaches every
|
|
48
|
+
* renderable this console draws. Default false.
|
|
49
|
+
*/
|
|
50
|
+
asciiOnly?: boolean;
|
|
44
51
|
/**
|
|
45
52
|
* Static width (cells). Ignored when `getSize` is provided. Falls back to
|
|
46
53
|
* the `COLUMNS` env var / the bound stream's `columns` / 80.
|
|
@@ -94,7 +101,7 @@ export interface ConsoleOptions {
|
|
|
94
101
|
export interface PrintOptions {
|
|
95
102
|
style?: string | Style;
|
|
96
103
|
justify?: "default" | "left" | "center" | "right" | "full";
|
|
97
|
-
overflow?:
|
|
104
|
+
overflow?: OverflowMethod;
|
|
98
105
|
highlight?: boolean;
|
|
99
106
|
markup?: boolean;
|
|
100
107
|
softWrap?: boolean;
|
|
@@ -115,6 +122,7 @@ export declare class Console {
|
|
|
115
122
|
private _highlight;
|
|
116
123
|
private _theme;
|
|
117
124
|
private _onStyleError;
|
|
125
|
+
private readonly _asciiOnly;
|
|
118
126
|
private _highlighter;
|
|
119
127
|
private _recorded;
|
|
120
128
|
private _capture;
|
|
@@ -125,7 +133,6 @@ export declare class Console {
|
|
|
125
133
|
};
|
|
126
134
|
get width(): number;
|
|
127
135
|
get height(): number;
|
|
128
|
-
get encoding(): string;
|
|
129
136
|
get isTerminal(): boolean;
|
|
130
137
|
get isInteractive(): boolean;
|
|
131
138
|
get colorSystem(): ColorDepth | null;
|
|
@@ -137,7 +144,9 @@ export declare class Console {
|
|
|
137
144
|
get destination(): Destination;
|
|
138
145
|
get file(): ConsoleSink;
|
|
139
146
|
get theme(): Theme;
|
|
140
|
-
get options(): RenderOptions
|
|
147
|
+
get options(): RenderOptions & {
|
|
148
|
+
height: Height;
|
|
149
|
+
};
|
|
141
150
|
print(...args: unknown[]): void;
|
|
142
151
|
log(...args: unknown[]): void;
|
|
143
152
|
rule(title?: string, options?: RuleOptions): void;
|
package/dist/core/console.js
CHANGED
|
@@ -16,7 +16,45 @@ import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
|
|
|
16
16
|
// rather than being restated here; see `rule()`.
|
|
17
17
|
import { Rule } from "../renderables/rule.js";
|
|
18
18
|
import { segmentsToString } from "./render.js";
|
|
19
|
+
import { placeBlock } from "./place.js";
|
|
19
20
|
import { isRenderable } from "./protocol.js";
|
|
21
|
+
// Three of the five justify methods place what a print draws as a block, the
|
|
22
|
+
// way Rich's `print` wraps each renderable in `Align`. The other two leave it
|
|
23
|
+
// where it was drawn: `"default"` asks for no placement, and `"full"` is a way
|
|
24
|
+
// of setting a paragraph's words, which only text can do.
|
|
25
|
+
const PLACED_BY = {
|
|
26
|
+
default: undefined,
|
|
27
|
+
full: undefined,
|
|
28
|
+
left: "left",
|
|
29
|
+
center: "center",
|
|
30
|
+
right: "right",
|
|
31
|
+
};
|
|
32
|
+
// A text run as one block: its items joined into one text, as the reference's
|
|
33
|
+
// `Text.join` joins them, drawn under the print's options and ended by the
|
|
34
|
+
// print's `end`. Measure and render both read that one text, so the width
|
|
35
|
+
// `justify` places the run at is the width it is drawn at. Each item's own
|
|
36
|
+
// `justify`, `overflow`, `noWrap` and `tabSize` stay behind, as they do in the
|
|
37
|
+
// reference: the print's options set the whole run.
|
|
38
|
+
class TextRun {
|
|
39
|
+
items;
|
|
40
|
+
end;
|
|
41
|
+
constructor(items, end) {
|
|
42
|
+
this.items = items;
|
|
43
|
+
this.end = end;
|
|
44
|
+
}
|
|
45
|
+
*render(options) {
|
|
46
|
+
yield* this.text(options).render(options);
|
|
47
|
+
yield this.end;
|
|
48
|
+
}
|
|
49
|
+
measure(options) {
|
|
50
|
+
return this.text(options).measure(options);
|
|
51
|
+
}
|
|
52
|
+
// A `Pretty` lays itself out for the width it is offered, so the run is
|
|
53
|
+
// joined per call rather than once.
|
|
54
|
+
text(options) {
|
|
55
|
+
return this.items.reduce((joined, item) => joined.append(item instanceof Pretty ? item.toText(options) : item), new RichText("", { end: "" }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
20
58
|
// A host with an environment but no streams — what a browser looks like from
|
|
21
59
|
// here. Frozen so the shared instance cannot be mutated into a fake terminal.
|
|
22
60
|
const DETACHED_ENVIRONMENT = Object.freeze({
|
|
@@ -67,8 +105,9 @@ function terminalSize(environment, stream) {
|
|
|
67
105
|
function defaultSink(stream) {
|
|
68
106
|
if (stream === undefined) {
|
|
69
107
|
throw new Error("Console: no `file` provided and the environment has no stream to " +
|
|
70
|
-
"write to (e.g. running in a browser). Pass
|
|
71
|
-
"
|
|
108
|
+
"write to (e.g. running in a browser). Pass " +
|
|
109
|
+
"`environment: hostEnvironment(host)` to write through a terminal " +
|
|
110
|
+
"host, or a `file` so output has somewhere to go.");
|
|
72
111
|
}
|
|
73
112
|
return stream;
|
|
74
113
|
}
|
|
@@ -146,6 +185,7 @@ export class Console {
|
|
|
146
185
|
_highlight;
|
|
147
186
|
_theme;
|
|
148
187
|
_onStyleError;
|
|
188
|
+
_asciiOnly;
|
|
149
189
|
_highlighter;
|
|
150
190
|
_recorded;
|
|
151
191
|
// [LAW:types-are-the-program] The capture state is carried in the *type*
|
|
@@ -182,6 +222,7 @@ export class Console {
|
|
|
182
222
|
this._highlight = options?.highlight !== false;
|
|
183
223
|
this._highlighter = options?.highlighter ?? new ReprHighlighter();
|
|
184
224
|
this._onStyleError = options?.onStyleError;
|
|
225
|
+
this._asciiOnly = options?.asciiOnly ?? false;
|
|
185
226
|
this._recorded = [];
|
|
186
227
|
this._capture = null;
|
|
187
228
|
}
|
|
@@ -195,9 +236,6 @@ export class Console {
|
|
|
195
236
|
get height() {
|
|
196
237
|
return this._getSize().height;
|
|
197
238
|
}
|
|
198
|
-
get encoding() {
|
|
199
|
-
return "utf-8";
|
|
200
|
-
}
|
|
201
239
|
get isTerminal() {
|
|
202
240
|
return this._isTerminal;
|
|
203
241
|
}
|
|
@@ -231,11 +269,16 @@ export class Console {
|
|
|
231
269
|
return this._theme;
|
|
232
270
|
}
|
|
233
271
|
get options() {
|
|
272
|
+
// One read of the size, so a resize cannot pair one frame's width with
|
|
273
|
+
// another's height.
|
|
274
|
+
const { width, height } = this.size;
|
|
234
275
|
return {
|
|
235
|
-
maxWidth:
|
|
276
|
+
maxWidth: width,
|
|
277
|
+
// The terminal an inline print lands on: a ceiling, never a region —
|
|
278
|
+
// content keeps its natural height beneath it.
|
|
279
|
+
height: { rows: height, exact: false },
|
|
236
280
|
isTerminal: this.isTerminal,
|
|
237
|
-
|
|
238
|
-
asciiOnly: false,
|
|
281
|
+
asciiOnly: this._asciiOnly,
|
|
239
282
|
theme: this._theme,
|
|
240
283
|
onStyleError: this._onStyleError,
|
|
241
284
|
colorSystem: this._destination.colorSystem,
|
|
@@ -335,53 +378,65 @@ export class Console {
|
|
|
335
378
|
else
|
|
336
379
|
blocks.push({ kind: "text", items: [text] });
|
|
337
380
|
}
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// `"ignore"` — which is why the two arrive at one field.
|
|
381
|
+
// Soft wrap is the reference's pair of defaults, not a mode of its own: it
|
|
382
|
+
// stops wrapping, and when no overflow was named it asks for `"ignore"`, so
|
|
383
|
+
// the line leaves unbounded. A named overflow still cuts the unwrapped line
|
|
384
|
+
// at the width, as Rich's does. What `"ignore"` does to a line — no edge,
|
|
385
|
+
// no justify — is `RichText`'s to apply, so it crosses as it was asked for.
|
|
344
386
|
const renderOpts = {
|
|
345
387
|
...this.options,
|
|
346
388
|
justify: opts.justify === "default" ? undefined : opts.justify,
|
|
347
|
-
overflow: opts.overflow
|
|
348
|
-
noWrap: softWrap
|
|
389
|
+
overflow: opts.overflow ?? (softWrap ? "ignore" : undefined),
|
|
390
|
+
noWrap: softWrap,
|
|
349
391
|
};
|
|
350
|
-
// The print style, then the console's base style, over
|
|
351
|
-
// and
|
|
352
|
-
// across the newline.
|
|
392
|
+
// The print style, then the console's base style, over each line a block
|
|
393
|
+
// drew and never over the break that ends it: a styled break carries SGR
|
|
394
|
+
// codes across the newline.
|
|
353
395
|
// [LAW:dataflow-not-control-flow] no style to apply is an empty list, not
|
|
354
396
|
// a skipped step.
|
|
355
397
|
const styles = [printStyle, this._style].filter((style) => !style.isNull);
|
|
356
398
|
const styleContent = (segments) => styles.reduce((styled, style) => Segment.applyStyle(styled, style), segments);
|
|
357
|
-
// Every line end goes through the same writeSegments funnel as the text it
|
|
358
|
-
// ends, so it survives recording — otherwise `exportText` and `exportHtml`
|
|
359
|
-
// would join consecutive prints onto a single line. [LAW:single-enforcer]
|
|
360
399
|
// The common default end is "\n" — reuse Segment's cached newline rather
|
|
361
400
|
// than allocating one per print; only a non-default end needs a fresh one.
|
|
362
401
|
const terminator = end === "\n" ? Segment.line() : new Segment(end);
|
|
402
|
+
const align = PLACED_BY[opts.justify ?? "default"];
|
|
403
|
+
// Each block as its lines, and whether its last line is closed. A placed
|
|
404
|
+
// block is closed line by line, as `Align` closes it. Unplaced, a text run
|
|
405
|
+
// is closed exactly where its content — its `end` included — breaks; any
|
|
406
|
+
// other block's lines are closed whether or not it closed them itself: a
|
|
407
|
+
// `Panel` ends in a line break and a bare `Spinner` or `ProgressBar` does
|
|
408
|
+
// not (they're line fragments, meant to be composed within a line —
|
|
409
|
+
// rich-flexstrip-5kf.4cq), and both leave the next print at the start of a
|
|
410
|
+
// line.
|
|
411
|
+
const drawBlock = (block) => {
|
|
412
|
+
const renderable = block.kind === "text" ? new TextRun(block.items, terminator) : block.renderable;
|
|
413
|
+
if (align)
|
|
414
|
+
return { lines: placeBlock(renderable, align, renderOpts), closed: true };
|
|
415
|
+
const drawn = [...renderable.render(renderOpts)];
|
|
416
|
+
// A text run's last segment is always its `end`, yielded even when
|
|
417
|
+
// empty, and `splitLines` drops the empty line after a final break only
|
|
418
|
+
// when nothing follows it — so the run is closed exactly when `end` is.
|
|
419
|
+
const closed = block.kind === "lines" || end.endsWith("\n");
|
|
420
|
+
return { lines: Segment.splitLines(drawn), closed };
|
|
421
|
+
};
|
|
422
|
+
// Every line end goes through the same writeSegments funnel as the text it
|
|
423
|
+
// ends, so it survives recording — otherwise `exportText` and `exportHtml`
|
|
424
|
+
// would join consecutive prints onto a single line. [LAW:single-enforcer]
|
|
363
425
|
const output = [];
|
|
364
426
|
for (const block of blocks) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// does not (they're line fragments, meant to be composed within a
|
|
372
|
-
// line — rich-flexstrip-5kf.4cq), and both leave the next print at
|
|
373
|
-
// the start of a line.
|
|
374
|
-
for (const line of Segment.splitLines(block.renderable.render(renderOpts))) {
|
|
375
|
-
output.push(...styleContent(line), Segment.line());
|
|
376
|
-
}
|
|
377
|
-
}
|
|
427
|
+
const { lines, closed } = drawBlock(block);
|
|
428
|
+
lines.forEach((line, index) => {
|
|
429
|
+
output.push(...styleContent(line));
|
|
430
|
+
if (closed || index < lines.length - 1)
|
|
431
|
+
output.push(Segment.line());
|
|
432
|
+
});
|
|
378
433
|
}
|
|
379
434
|
// Crop last, and crop the line-end with the rest: in the reference `end` is
|
|
380
435
|
// the tail of the printed line, so a line cut at the edge loses it too.
|
|
381
436
|
// Soft wrap turns cropping off whatever `crop` says, because a line it left
|
|
382
437
|
// whole is meant to reach the terminal whole. [LAW:dataflow-not-control-flow]
|
|
383
438
|
// Not cropping is an unbounded width rather than a skipped step — the same
|
|
384
|
-
// spelling `RichText` uses for `
|
|
439
|
+
// spelling `RichText` uses for `"ignore"`.
|
|
385
440
|
const cropWidth = !softWrap && (opts.crop ?? true) ? this.width : Infinity;
|
|
386
441
|
this._writeSegments([...Segment.cropLines(output, cropWidth)]);
|
|
387
442
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* export-html — recorded segments drawn as a
|
|
2
|
+
* export-html — recorded segments drawn as HTML: a fragment that can sit in
|
|
3
|
+
* a page this library does not own, and a standalone document around it.
|
|
3
4
|
*
|
|
4
5
|
* An encoding of `export-lines` and nothing more. What a run looks like under
|
|
5
6
|
* a theme is decided there; this module decides only how CSS says it. It never
|
|
@@ -14,11 +15,45 @@
|
|
|
14
15
|
import type { TerminalTheme } from "./color.js";
|
|
15
16
|
import type { Segment } from "./segment.js";
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
+
* The CSS a page includes once, wherever fragments appear: what an inline
|
|
19
|
+
* style cannot say. A fragment that blinks draws steadily without it.
|
|
20
|
+
*
|
|
21
|
+
* The keyframes exist only for a reader who has not asked for reduced motion,
|
|
22
|
+
* so for one who has, the inline `animation` names nothing and the glyph holds
|
|
23
|
+
* still. One rule, so no second rule's order or survival in a host's CSS
|
|
24
|
+
* pipeline can bring the blink back.
|
|
25
|
+
*/
|
|
26
|
+
export declare const HTML_FRAGMENT_CSS = "@media (prefers-reduced-motion:no-preference){@keyframes rich-blink{50%{color:transparent}}}";
|
|
27
|
+
/**
|
|
28
|
+
* `segments` under `theme` as one `pre` carrying its canvas, for embedding.
|
|
29
|
+
*
|
|
30
|
+
* [LAW:locality-or-seam] Every rule is inline on the `pre` or below it, so the
|
|
31
|
+
* fragment styles nothing outside itself; the host page keeps its own `body`,
|
|
32
|
+
* `pre` and `a` rules. The seam holds the other way too: `all:initial` stops
|
|
33
|
+
* the host's `pre` rules and inherited typography from reaching the rows, and
|
|
34
|
+
* the two properties `all` does not cover pin the rows left to right.
|
|
35
|
+
*
|
|
36
|
+
* The one way in is the `--rich-fragment-font` custom property, read as the
|
|
37
|
+
* `font` shorthand (`14px/1.3 "JetBrains Mono", monospace`). `all` resets no
|
|
38
|
+
* custom property, so a host sets it on any ancestor to draw the rows in its
|
|
39
|
+
* own code font; unset, the rows are the browser's default monospace at a
|
|
40
|
+
* fixed line height, as a terminal's rows are. A host's value wants one too: at
|
|
41
|
+
* `normal`, a row holding a glyph from a taller fallback font grows and shifts
|
|
42
|
+
* every row below it. It must be a whole shorthand, a size and a family at
|
|
43
|
+
* least: any other value is invalid when computed, and the rows then inherit
|
|
44
|
+
* the host's font.
|
|
18
45
|
*
|
|
19
46
|
* A browser draws no line for a newline at either edge of a `pre`: the parser
|
|
20
|
-
* drops the one straight after
|
|
21
|
-
* line without starting another. So the
|
|
22
|
-
* and every row ends with one, and a blank first or last row is still drawn.
|
|
47
|
+
* drops the one straight after the open tag, and the one before `</pre>` ends a
|
|
48
|
+
* line without starting another. So the fragment opens with a newline of its
|
|
49
|
+
* own and every row ends with one, and a blank first or last row is still drawn.
|
|
50
|
+
*/
|
|
51
|
+
export declare function encodeHtmlFragment(segments: Iterable<Segment>, theme?: TerminalTheme): string;
|
|
52
|
+
/**
|
|
53
|
+
* `segments` under `theme` as a complete HTML document.
|
|
54
|
+
*
|
|
55
|
+
* [LAW:one-source-of-truth] The document is the fragment in a shell, so the
|
|
56
|
+
* two cannot draw different pictures. The shell paints the page around the
|
|
57
|
+
* `pre` in the same canvas and includes the fragment CSS once.
|
|
23
58
|
*/
|
|
24
59
|
export declare function encodeHtml(segments: Iterable<Segment>, theme?: TerminalTheme): string;
|
package/dist/core/export-html.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* export-html — recorded segments drawn as a
|
|
2
|
+
* export-html — recorded segments drawn as HTML: a fragment that can sit in
|
|
3
|
+
* a page this library does not own, and a standalone document around it.
|
|
3
4
|
*
|
|
4
5
|
* An encoding of `export-lines` and nothing more. What a run looks like under
|
|
5
6
|
* a theme is decided there; this module decides only how CSS says it. It never
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
* them: `white-space: pre`, and a scrollbar rather than a reflow when the page
|
|
12
13
|
* is narrower than the terminal was.
|
|
13
14
|
*/
|
|
15
|
+
import { cellLen, graphemes } from "./cells.js";
|
|
14
16
|
import { exportCanvas, exportLines } from "./export-lines.js";
|
|
15
17
|
const ENTITIES = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
16
18
|
const escapeText = (text) => text.replace(/[&<>]/g, (c) => ENTITIES[c]);
|
|
@@ -64,7 +66,53 @@ function glyphCss(look) {
|
|
|
64
66
|
...OUTLINE[look.outline],
|
|
65
67
|
];
|
|
66
68
|
}
|
|
69
|
+
// `unset` hands the anchor the run's look and drops the host's `a` rules;
|
|
70
|
+
// `revert` gives back the browser's link cursor and focus ring.
|
|
71
|
+
const ANCHOR_CSS = "all:unset;cursor:revert;outline:revert";
|
|
67
72
|
const span = (css, content) => `<span style="${escapeAttribute(css.join(";"))}">${content}</span>`;
|
|
73
|
+
/** Text of printable ASCII only: one cell a character, nothing to box. */
|
|
74
|
+
const ONE_CELL = /^[\x20-\x7e]*$/;
|
|
75
|
+
/**
|
|
76
|
+
* A box exactly `cells` of the monospace font wide (`ch` is one of its cells)
|
|
77
|
+
* for glyphs the browser would otherwise draw at a fallback font's width,
|
|
78
|
+
* which for a CJK character is rarely two cells: left to it, everything after
|
|
79
|
+
* them on the row shifts off its column. The box carries no paint, so the
|
|
80
|
+
* run's background runs through it at the run's own height; an atomic inline
|
|
81
|
+
* takes no decoration from its ancestors, so it inherits its parent's lines.
|
|
82
|
+
*/
|
|
83
|
+
const cellBox = (cells) => ["display:inline-block", `width:${cells}ch`, "text-align:center", "text-decoration:inherit"];
|
|
84
|
+
/**
|
|
85
|
+
* `text` with each stretch of graphemes wider than one cell in one box as wide
|
|
86
|
+
* as their cells together, and `draw` applied to every stretch of escaped text
|
|
87
|
+
* inside and between the boxes. A stretch, not a glyph apiece, because a
|
|
88
|
+
* browser's find-in-page does not match across two boxes. Graphemes, not code
|
|
89
|
+
* points, because `cellLen` measures a joined emoji as one glyph; a zero-width
|
|
90
|
+
* one joins the stretch before it, so it can still combine.
|
|
91
|
+
*/
|
|
92
|
+
function onGrid(text, draw) {
|
|
93
|
+
if (ONE_CELL.test(text))
|
|
94
|
+
return draw(escapeText(text));
|
|
95
|
+
const pieces = [];
|
|
96
|
+
let stretch = "";
|
|
97
|
+
let wideCells = 0;
|
|
98
|
+
const flush = () => {
|
|
99
|
+
if (stretch !== "")
|
|
100
|
+
pieces.push(wideCells === 0 ? draw(escapeText(stretch)) : span(cellBox(wideCells), draw(escapeText(stretch))));
|
|
101
|
+
stretch = "";
|
|
102
|
+
wideCells = 0;
|
|
103
|
+
};
|
|
104
|
+
for (const segment of graphemes(text)) {
|
|
105
|
+
const cells = cellLen(segment);
|
|
106
|
+
const wide = cells === 0 ? wideCells > 0 : cells > 1;
|
|
107
|
+
if (wide !== (wideCells > 0))
|
|
108
|
+
flush();
|
|
109
|
+
stretch += segment;
|
|
110
|
+
if (wide)
|
|
111
|
+
wideCells += cells;
|
|
112
|
+
}
|
|
113
|
+
flush();
|
|
114
|
+
return pieces.join("");
|
|
115
|
+
}
|
|
68
116
|
/**
|
|
69
117
|
* One run as markup.
|
|
70
118
|
*
|
|
@@ -72,35 +120,81 @@ const span = (css, content) => `<span style="${escapeAttribute(css.join(";"))}">
|
|
|
72
120
|
* span with a strike would double the strike too. It gets an outer span of its
|
|
73
121
|
* own instead. That span carries the paint, because a descendant's background
|
|
74
122
|
* may be painted over an ancestor's underline, and the blink, so the underline
|
|
75
|
-
* blinks with the glyph.
|
|
123
|
+
* blinks with the glyph. A wide glyph's box sits between the two: it inherits
|
|
124
|
+
* the double underline, and the inner span inside it draws the other lines.
|
|
76
125
|
*/
|
|
77
126
|
function runHtml({ text, look }) {
|
|
78
|
-
const glyph = escapeText(text);
|
|
79
127
|
const drawn = look.underline === "double"
|
|
80
|
-
? span([...paintCss(look), "text-decoration-line:underline", "text-decoration-style:double", ...BLINK[look.blink]], span([`color:${look.foreground.hex}`, ...glyphCss(look)], glyph))
|
|
81
|
-
: span([...paintCss(look), ...glyphCss(look)], glyph);
|
|
82
|
-
return look.href === null
|
|
128
|
+
? span([...paintCss(look), "text-decoration-line:underline", "text-decoration-style:double", ...BLINK[look.blink]], onGrid(text, (glyph) => span([`color:${look.foreground.hex}`, ...glyphCss(look)], glyph)))
|
|
129
|
+
: span([...paintCss(look), ...glyphCss(look)], onGrid(text, (glyph) => glyph));
|
|
130
|
+
return look.href === null
|
|
131
|
+
? drawn
|
|
132
|
+
: `<a href="${escapeAttribute(look.href)}" style="${ANCHOR_CSS}">${drawn}</a>`;
|
|
83
133
|
}
|
|
84
134
|
/**
|
|
85
|
-
*
|
|
135
|
+
* The CSS a page includes once, wherever fragments appear: what an inline
|
|
136
|
+
* style cannot say. A fragment that blinks draws steadily without it.
|
|
137
|
+
*
|
|
138
|
+
* The keyframes exist only for a reader who has not asked for reduced motion,
|
|
139
|
+
* so for one who has, the inline `animation` names nothing and the glyph holds
|
|
140
|
+
* still. One rule, so no second rule's order or survival in a host's CSS
|
|
141
|
+
* pipeline can bring the blink back.
|
|
142
|
+
*/
|
|
143
|
+
export const HTML_FRAGMENT_CSS = `@media (prefers-reduced-motion:no-preference){@keyframes ${BLINK_KEYFRAMES}{50%{color:transparent}}}`;
|
|
144
|
+
/**
|
|
145
|
+
* `segments` under `theme` as one `pre` carrying its canvas, for embedding.
|
|
146
|
+
*
|
|
147
|
+
* [LAW:locality-or-seam] Every rule is inline on the `pre` or below it, so the
|
|
148
|
+
* fragment styles nothing outside itself; the host page keeps its own `body`,
|
|
149
|
+
* `pre` and `a` rules. The seam holds the other way too: `all:initial` stops
|
|
150
|
+
* the host's `pre` rules and inherited typography from reaching the rows, and
|
|
151
|
+
* the two properties `all` does not cover pin the rows left to right.
|
|
152
|
+
*
|
|
153
|
+
* The one way in is the `--rich-fragment-font` custom property, read as the
|
|
154
|
+
* `font` shorthand (`14px/1.3 "JetBrains Mono", monospace`). `all` resets no
|
|
155
|
+
* custom property, so a host sets it on any ancestor to draw the rows in its
|
|
156
|
+
* own code font; unset, the rows are the browser's default monospace at a
|
|
157
|
+
* fixed line height, as a terminal's rows are. A host's value wants one too: at
|
|
158
|
+
* `normal`, a row holding a glyph from a taller fallback font grows and shifts
|
|
159
|
+
* every row below it. It must be a whole shorthand, a size and a family at
|
|
160
|
+
* least: any other value is invalid when computed, and the rows then inherit
|
|
161
|
+
* the host's font.
|
|
86
162
|
*
|
|
87
163
|
* A browser draws no line for a newline at either edge of a `pre`: the parser
|
|
88
|
-
* drops the one straight after
|
|
89
|
-
* line without starting another. So the
|
|
90
|
-
* and every row ends with one, and a blank first or last row is still drawn.
|
|
164
|
+
* drops the one straight after the open tag, and the one before `</pre>` ends a
|
|
165
|
+
* line without starting another. So the fragment opens with a newline of its
|
|
166
|
+
* own and every row ends with one, and a blank first or last row is still drawn.
|
|
91
167
|
*/
|
|
92
|
-
export function
|
|
168
|
+
export function encodeHtmlFragment(segments, theme) {
|
|
93
169
|
const canvas = exportCanvas(theme);
|
|
94
170
|
const rows = exportLines(segments, theme).map((row) => `${row.map(runHtml).join("")}\n`);
|
|
95
171
|
const css = [
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
172
|
+
"all:initial",
|
|
173
|
+
"direction:ltr",
|
|
174
|
+
"unicode-bidi:isolate",
|
|
175
|
+
"display:block",
|
|
176
|
+
`background:${canvas.background.hex}`,
|
|
177
|
+
`color:${canvas.foreground.hex}`,
|
|
178
|
+
"padding:1em",
|
|
179
|
+
"font:var(--rich-fragment-font,medium/1.2 monospace)",
|
|
180
|
+
"white-space:pre",
|
|
181
|
+
"overflow-x:auto",
|
|
182
|
+
].join(";");
|
|
183
|
+
return `<pre style="${css}">\n${rows.join("")}</pre>`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* `segments` under `theme` as a complete HTML document.
|
|
187
|
+
*
|
|
188
|
+
* [LAW:one-source-of-truth] The document is the fragment in a shell, so the
|
|
189
|
+
* two cannot draw different pictures. The shell paints the page around the
|
|
190
|
+
* `pre` in the same canvas and includes the fragment CSS once.
|
|
191
|
+
*/
|
|
192
|
+
export function encodeHtml(segments, theme) {
|
|
193
|
+
const canvas = exportCanvas(theme);
|
|
194
|
+
const css = `body{background:${canvas.background.hex};color:${canvas.foreground.hex};margin:0}\n${HTML_FRAGMENT_CSS}`;
|
|
101
195
|
return [
|
|
102
196
|
"<!DOCTYPE html>",
|
|
103
197
|
`<html><head><meta charset="utf-8"><style>\n${css}\n</style></head>`,
|
|
104
|
-
`<body
|
|
198
|
+
`<body>${encodeHtmlFragment(segments, theme)}</body></html>`,
|
|
105
199
|
].join("\n");
|
|
106
200
|
}
|
package/dist/core/markup.d.ts
CHANGED
|
@@ -62,12 +62,35 @@ export declare class MarkupSyntaxError extends MarkupError {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Escapes markup characters so they render as literal text.
|
|
65
|
+
*
|
|
66
|
+
* [LAW:one-source-of-truth] The inverse of the two ways the parser reads a
|
|
67
|
+
* backslash run in front of a bracket, over the same `TAG_BODY`: before a tag
|
|
68
|
+
* the run is halved and an odd one left over escapes it, so the run is doubled
|
|
69
|
+
* and one added; before any other bracket one `\[` becomes `[`, so one
|
|
70
|
+
* backslash is added.
|
|
71
|
+
*
|
|
72
|
+
* No bracket in the output can open a tag, whatever it is joined to. A bracket
|
|
73
|
+
* whose tag the text ends before finishing — `[link=x` — is escaped as a tag,
|
|
74
|
+
* because the next fragment's `]` would finish it; when what follows is instead
|
|
75
|
+
* the caller's own tag, a backslash run in front of it renders doubled. A
|
|
76
|
+
* trailing run is doubled for the same reason: `escape("C:\\")` spliced before
|
|
77
|
+
* `[/bold]` would otherwise escape the closing tag, so at the very end of a
|
|
78
|
+
* string it renders doubled. The grammar cannot tell those cases apart, and
|
|
79
|
+
* both choices fail visibly rather than let input open or escape a tag.
|
|
80
|
+
*
|
|
81
|
+
* Rich's `escape` escapes only a finished tag and doubles only a trailing run
|
|
82
|
+
* of one, so input can drop a backslash, escape the closing tag after it, or
|
|
83
|
+
* open a tag across a join. All three are departures on purpose.
|
|
65
84
|
*/
|
|
66
85
|
export declare function escape(text: string): string;
|
|
67
86
|
export interface MarkupTagContext {
|
|
68
87
|
/** Parsed `key=value` attributes from the opening tag. */
|
|
69
88
|
attrs: Record<string, string>;
|
|
70
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Inner markup, already parsed (registry-aware) into a `RichText`. It carries
|
|
91
|
+
* no `baseStyle`: that is painted once, under the whole output, so it sits
|
|
92
|
+
* beneath whatever the handler returns.
|
|
93
|
+
*/
|
|
71
94
|
children: RichText;
|
|
72
95
|
/** Raw inner markup text (between the opening tag's `]` and the closing tag's `[`). */
|
|
73
96
|
raw: string;
|
|
@@ -89,9 +112,8 @@ export interface RenderMarkupOptions {
|
|
|
89
112
|
/**
|
|
90
113
|
* Plugin-aware markup render. Always returns a `RichText`. Built-in style
|
|
91
114
|
* tags become spans; registered plugin tags are resolved by their handler,
|
|
92
|
-
* whose returned `RichText`
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* append path is uniform.
|
|
115
|
+
* whose returned `RichText` stands in the output where its pair stood, under
|
|
116
|
+
* any style tag that encloses the pair. The recursion is into the inner slice
|
|
117
|
+
* of each plugin pair, and every recursion node returns a `RichText`.
|
|
96
118
|
*/
|
|
97
119
|
export declare function renderMarkup(markup: string, options?: RenderMarkupOptions): RichText;
|