@promptctl/rich-js 0.26.0 → 0.28.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 +1 -0
- package/THIRD-PARTY-NOTICES +42 -0
- package/dist/core/animation.d.ts +52 -15
- package/dist/core/animation.js +82 -37
- package/dist/core/ansi.d.ts +15 -8
- package/dist/core/ansi.js +106 -28
- package/dist/core/bidi-data.d.ts +14 -0
- package/dist/core/bidi-data.js +385 -0
- package/dist/core/bidi.d.ts +89 -0
- package/dist/core/bidi.js +615 -0
- package/dist/core/box.d.ts +9 -1
- package/dist/core/box.js +11 -0
- package/dist/core/cell-widths.d.ts +3 -2
- package/dist/core/cell-widths.js +93 -93
- package/dist/core/cells.d.ts +20 -0
- package/dist/core/cells.js +93 -5
- package/dist/core/color.d.ts +26 -18
- package/dist/core/color.js +37 -34
- package/dist/core/console.d.ts +26 -1
- package/dist/core/console.js +26 -6
- package/dist/core/control-codes.d.ts +61 -0
- package/dist/core/control-codes.js +157 -0
- package/dist/core/easing.js +6 -1
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.js +9 -1
- package/dist/core/export-html.js +4 -3
- package/dist/core/export-lines.d.ts +2 -0
- package/dist/core/export-lines.js +7 -3
- package/dist/core/export-svg.js +6 -5
- package/dist/core/json.js +9 -2
- package/dist/core/markup.js +4 -3
- package/dist/core/oklch.d.ts +2 -0
- package/dist/core/oklch.js +3 -3
- package/dist/core/orient.d.ts +68 -0
- package/dist/core/orient.js +224 -0
- package/dist/core/osc8.d.ts +12 -5
- package/dist/core/osc8.js +14 -7
- package/dist/core/pretty.js +22 -18
- package/dist/core/protocol.d.ts +53 -22
- package/dist/core/protocol.js +14 -6
- package/dist/core/render.d.ts +8 -3
- package/dist/core/render.js +19 -5
- package/dist/core/seal.d.ts +26 -0
- package/dist/core/seal.js +75 -0
- package/dist/core/segment.d.ts +9 -1
- package/dist/core/segment.js +11 -1
- package/dist/core/style.d.ts +29 -1
- package/dist/core/style.js +41 -5
- package/dist/core/text-order.d.ts +51 -0
- package/dist/core/text-order.js +50 -0
- package/dist/core/text.d.ts +10 -0
- package/dist/core/text.js +133 -87
- package/dist/core/timeline.js +6 -6
- package/dist/core/view.d.ts +23 -9
- package/dist/core/view.js +37 -12
- package/dist/core/wrap.d.ts +24 -6
- package/dist/core/wrap.js +82 -16
- package/dist/host/app.d.ts +48 -7
- package/dist/host/app.js +116 -11
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +35 -2
- package/dist/host/input.js +88 -4
- package/dist/host/terminal-colors.d.ts +18 -0
- package/dist/host/terminal-colors.js +46 -0
- package/dist/host/terminal-host.d.ts +2 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +2 -2
- package/dist/renderables/change.d.ts +1 -3
- package/dist/renderables/change.js +4 -6
- package/dist/renderables/effect.d.ts +61 -60
- package/dist/renderables/effect.js +91 -81
- package/dist/renderables/effects.d.ts +37 -8
- package/dist/renderables/effects.js +193 -55
- package/dist/renderables/frames.d.ts +1 -10
- package/dist/renderables/frames.js +1 -16
- package/dist/renderables/glyph-effect.d.ts +12 -13
- package/dist/renderables/glyph-effect.js +34 -32
- package/dist/renderables/glyph-effects.d.ts +27 -8
- package/dist/renderables/glyph-effects.js +75 -7
- package/dist/renderables/lifecycle.d.ts +40 -32
- package/dist/renderables/lifecycle.js +87 -48
- package/dist/renderables/live.d.ts +3 -3
- package/dist/renderables/live.js +4 -4
- package/dist/renderables/markdown.js +4 -2
- package/dist/renderables/panel.js +6 -4
- package/dist/renderables/progress.d.ts +11 -4
- package/dist/renderables/progress.js +29 -17
- package/dist/renderables/progressBar.d.ts +3 -3
- package/dist/renderables/progressBar.js +16 -13
- package/dist/renderables/syntax.js +7 -2
- package/dist/renderables/table.js +2 -1
- package/dist/template-bindings/animation-funcs.d.ts +49 -18
- package/dist/template-bindings/animation-funcs.js +115 -32
- package/dist/template-bindings/index.d.ts +7 -5
- package/dist/template-bindings/index.js +8 -5
- package/dist/template-bindings/palette-funcs.d.ts +1 -1
- package/dist/template-bindings/palette-funcs.js +1 -1
- package/dist/themes/buildPalette.d.ts +15 -39
- package/dist/themes/buildPalette.js +196 -67
- package/dist/themes/colorMath.d.ts +10 -4
- package/dist/themes/colorMath.js +33 -1
- package/dist/themes/data/index.js +1 -4
- package/dist/themes/data/textual-ansi.js +0 -143
- package/dist/themes/data/types.d.ts +3 -1
- package/dist/themes/palette.d.ts +22 -6
- package/dist/themes/palette.js +13 -21
- package/dist/themes/ramp.d.ts +1 -1
- package/dist/themes/ramp.js +1 -1
- package/dist/themes/registry.d.ts +7 -10
- package/dist/themes/registry.js +9 -30
- package/dist/themes/terminalThemes.d.ts +4 -10
- package/dist/themes/terminalThemes.js +31 -46
- package/dist/themes/transpose.d.ts +5 -10
- package/dist/themes/transpose.js +13 -24
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +6 -6
- package/dist/widgets/checkbox.js +4 -4
- package/dist/widgets/dropdown.d.ts +1 -1
- package/dist/widgets/dropdown.js +7 -5
- package/dist/widgets/event-router.js +2 -2
- package/dist/widgets/ink.d.ts +5 -4
- package/dist/widgets/ink.js +6 -9
- package/dist/widgets/text-input.d.ts +1 -1
- package/dist/widgets/text-input.js +41 -32
- package/dist/widgets/themed-widget.js +3 -3
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +5 -5
- package/dist/widgets/types.d.ts +2 -2
- package/dist/widgets/widget-app.d.ts +3 -3
- package/dist/widgets/widget-app.js +5 -5
- package/package.json +5 -4
package/dist/core/protocol.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Segment } from "./segment.js";
|
|
6
6
|
import { Style, type Hold, type StyleSyntaxError, type Theme } from "./style.js";
|
|
7
|
-
import { ColorDepth } from "./color.js";
|
|
7
|
+
import { ColorDepth, type TerminalColors } from "./color.js";
|
|
8
|
+
import type { TextOrder } from "./text-order.js";
|
|
8
9
|
import type { Highlighter } from "./highlighter.js";
|
|
9
10
|
/**
|
|
10
11
|
* What becomes of a line still wider than its width once wrapping is done.
|
|
@@ -18,13 +19,27 @@ import type { Highlighter } from "./highlighter.js";
|
|
|
18
19
|
*/
|
|
19
20
|
export type OverflowMethod = "fold" | "crop" | "ellipsis" | "ignore";
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* The frame being drawn, as a value: `time`, the moment it is drawn for, in
|
|
23
|
+
* seconds, on the clock its cells' `nextChange` stamps are read on, and
|
|
24
|
+
* `host`, the identity of what drew it — nothing to read, so what draws in
|
|
25
|
+
* the frame has one time, this one, and cannot ask the host for another.
|
|
26
|
+
* [LAW:types-are-the-program] A `Progress` keeps the identity to refuse a
|
|
27
|
+
* frame of a host other than the one its tasks are timed on.
|
|
28
|
+
*/
|
|
29
|
+
export interface HostFrame {
|
|
30
|
+
readonly time: number;
|
|
31
|
+
readonly host: object;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What draws frames — a `Live`, an `App` — as what runs between them sees
|
|
35
|
+
* it: the frame it would draw now, its time the host's clock read through
|
|
36
|
+
* its timeline (`frameNow`, `./view.ts`). Not a `Clock`: under a paused
|
|
37
|
+
* timeline it holds, and under a reversed one it runs backwards. Asked
|
|
38
|
+
* inside a render it throws (`RenderClockError`): a frame has the time it was
|
|
39
|
+
* drawn for, and a renderable reads that one.
|
|
25
40
|
*/
|
|
26
41
|
export interface FrameHost {
|
|
27
|
-
|
|
42
|
+
frameNow(): HostFrame;
|
|
28
43
|
}
|
|
29
44
|
export interface RenderOptions {
|
|
30
45
|
/**
|
|
@@ -83,6 +98,17 @@ export interface RenderOptions {
|
|
|
83
98
|
* handed. Absent means truecolor.
|
|
84
99
|
*/
|
|
85
100
|
colorSystem?: ColorDepth | null;
|
|
101
|
+
/** What the output's destination does with direction. Absent is `"plain"`. */
|
|
102
|
+
textOrder?: TextOrder;
|
|
103
|
+
/**
|
|
104
|
+
* The terminal the output is drawn on: the colours a cell that sets none is
|
|
105
|
+
* drawn in, and the sixteen an ANSI colour names. The terminal's own palette
|
|
106
|
+
* is never queried, so the program says what it is, once, where the output
|
|
107
|
+
* is decided; a renderable reads it here (`Effected`, `ProgressBar`) and
|
|
108
|
+
* nothing hands it down to one. Absent is the terminal the depth assumes
|
|
109
|
+
* (`resolveTerminal`).
|
|
110
|
+
*/
|
|
111
|
+
terminal?: TerminalColors;
|
|
86
112
|
/**
|
|
87
113
|
* The names a style string may use. A `Console` passes its own; absent, the
|
|
88
114
|
* built-in defaults apply. Read it through `getStyle` rather than directly.
|
|
@@ -97,26 +123,22 @@ export interface RenderOptions {
|
|
|
97
123
|
onStyleError?: StyleErrorHandler;
|
|
98
124
|
/**
|
|
99
125
|
* The reader asked for reduced motion — CSS's `prefers-reduced-motion`,
|
|
100
|
-
* which a terminal has no way to report, so a host is told.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
126
|
+
* which a terminal has no way to report, so a host is told. Every placed
|
|
127
|
+
* animation is drawn reduced, its motion taken out and its content kept on
|
|
128
|
+
* time (`shownAt`, `Animation.reduced`, `./animation.ts`), and a `Destination`
|
|
129
|
+
* told the same writes no blink (`shownOn`, `./render.ts`). Absent, everything
|
|
130
|
+
* moves. Nothing here reads it from the environment.
|
|
105
131
|
*/
|
|
106
132
|
reducedMotion?: boolean;
|
|
107
133
|
/**
|
|
108
|
-
* The frame being drawn
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* nothing drawn from it moves.
|
|
134
|
+
* The frame being drawn (`HostFrame`): how a renderable that lives across
|
|
135
|
+
* frames — a widget, a `Progress` — draws for the time without being
|
|
136
|
+
* rebuilt each frame with one, and without reading a clock. A host writes
|
|
137
|
+
* it once a frame, and a print writes the one its caller named
|
|
138
|
+
* (`PrintOptions.frame`). Absent, the frame is drawn for no moment, 0
|
|
139
|
+
* (`frameTimeOf`, `./animation.ts`), and nothing drawn from it moves.
|
|
115
140
|
*/
|
|
116
|
-
frame?:
|
|
117
|
-
readonly time: number;
|
|
118
|
-
readonly host: FrameHost;
|
|
119
|
-
};
|
|
141
|
+
frame?: HostFrame;
|
|
120
142
|
/**
|
|
121
143
|
* Told of every renderable `render` is asked for, as it is asked, so the
|
|
122
144
|
* calls arrive in document order: a container before what it holds, what
|
|
@@ -138,6 +160,15 @@ export interface RenderOptions {
|
|
|
138
160
|
* decides the frame's layout, so when it changes, the frame may.
|
|
139
161
|
*/
|
|
140
162
|
onHold?: (hold: Hold) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Told of the hold the element `key` names, drawn for a time on its own
|
|
165
|
+
* clock, is in: an effect's frame, which alone knows, since the answer
|
|
166
|
+
* depends on what it drew. `t` is that clock's, not the frame's, so this cannot be `onHold`:
|
|
167
|
+
* the animation placed around the value carries the answer back through the
|
|
168
|
+
* combinators that remapped the time (`Moves`, `./animation.ts`). It reaches
|
|
169
|
+
* whatever `frame` reaches.
|
|
170
|
+
*/
|
|
171
|
+
onMoved?: (t: number, key: string, hold: Hold) => void;
|
|
141
172
|
}
|
|
142
173
|
/** The one field of the render options that says whether the reader wants motion. */
|
|
143
174
|
export type Motion = Pick<RenderOptions, "reducedMotion">;
|
package/dist/core/protocol.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
|
|
4
4
|
*/
|
|
5
5
|
import { cellCount } from "./cells.js";
|
|
6
|
-
import { DEFAULT_THEME, Style } from "./style.js";
|
|
6
|
+
import { DEFAULT_THEME, NEVER_CHANGES, Style } from "./style.js";
|
|
7
7
|
/** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
|
|
8
8
|
export const isAscii = (text) => /^[\x00-\x7F]*$/.test(text);
|
|
9
9
|
export function drawable(options, chosen, ascii, glyphs = String) {
|
|
@@ -262,7 +262,9 @@ export function render(renderable, options) {
|
|
|
262
262
|
* enters and takes some. A motion held at no width therefore wakes its host
|
|
263
263
|
* for frames that look the same: when it next takes room is not a moment its
|
|
264
264
|
* hold can name, and an early answer costs a frame where a late one hides the
|
|
265
|
-
* element.
|
|
265
|
+
* element. An effect is not such a motion: it cannot give itself room, so
|
|
266
|
+
* however it moves, a held one reports held for good, and whatever gives it
|
|
267
|
+
* room wakes the host. It commits nothing, as a measurement commits nothing —
|
|
266
268
|
* its effects (`afterPass`) are learned at a width nobody sees, so a
|
|
267
269
|
* `Viewport` it shows content through keeps the scroll the last frame shown
|
|
268
270
|
* left it.
|
|
@@ -273,10 +275,15 @@ export function render(renderable, options) {
|
|
|
273
275
|
* can repeat.
|
|
274
276
|
*/
|
|
275
277
|
function holding(options) {
|
|
276
|
-
const { onPresent } = options;
|
|
278
|
+
const { onPresent, onMoved } = options;
|
|
277
279
|
return {
|
|
278
280
|
phase: { pass: phaseHere().pass, effects: [] },
|
|
279
|
-
drawing: {
|
|
281
|
+
drawing: {
|
|
282
|
+
...options,
|
|
283
|
+
maxWidth: 0,
|
|
284
|
+
onPresent: onPresent && ((content) => onPresent(content, "held")),
|
|
285
|
+
onMoved: onMoved && ((t, key) => onMoved(t, key, NEVER_CHANGES)),
|
|
286
|
+
},
|
|
280
287
|
};
|
|
281
288
|
}
|
|
282
289
|
/**
|
|
@@ -295,13 +302,14 @@ export function measure(measurable, options) {
|
|
|
295
302
|
* `options` as a measurement is asked with. A `measure` that renders to learn
|
|
296
303
|
* its shape draws nothing on the frame, so it reports nothing to `onPresent` —
|
|
297
304
|
* one that did would put a widget in the view at the moment a container
|
|
298
|
-
* measured it, and keep one the frame then cropped away
|
|
305
|
+
* measured it, and keep one the frame then cropped away — and nothing to
|
|
306
|
+
* `onMoved`, which speaks only for what the frame shows. It still tells its
|
|
299
307
|
* hold (`onHold`): a layout decided by a measurement changes when it does.
|
|
300
308
|
*
|
|
301
309
|
* [LAW:single-enforcer] Every measurement starts here: `measure` asks with it.
|
|
302
310
|
*/
|
|
303
311
|
function measuring(options) {
|
|
304
|
-
return { ...options, onPresent: undefined };
|
|
312
|
+
return { ...options, onPresent: undefined, onMoved: undefined };
|
|
305
313
|
}
|
|
306
314
|
/**
|
|
307
315
|
* The options a renderable should *lay out against*: `withCellWidth`, with an
|
package/dist/core/render.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ import type { ColorSystemName, Destination } from "./color.js";
|
|
|
49
49
|
import type { Env } from "./env.js";
|
|
50
50
|
import { Segment } from "./segment.js";
|
|
51
51
|
import type { Renderable, RenderOptions } from "./protocol.js";
|
|
52
|
-
export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "theme" | "onStyleError" | "highlight" | "highlighter"> {
|
|
52
|
+
export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "textOrder" | "terminal" | "theme" | "onStyleError" | "highlight" | "highlighter" | "frame"> {
|
|
53
53
|
/** Cell width to render into. Default 80. */
|
|
54
54
|
width?: number;
|
|
55
55
|
/**
|
|
@@ -92,9 +92,14 @@ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" |
|
|
|
92
92
|
export declare function shownOn(segment: Segment, destination: Destination): Segment;
|
|
93
93
|
/**
|
|
94
94
|
* Encodes one segment as ANSI bytes for `destination`: the reference's
|
|
95
|
-
* `Style.render` for that segment, and nothing for a control segment.
|
|
95
|
+
* `Style.render` for that segment, and nothing for a control segment. It seals
|
|
96
|
+
* the segment alone, and a `logical` row's isolate spans its segments, so a row
|
|
97
|
+
* goes through `segmentsToString` whole.
|
|
96
98
|
*/
|
|
97
99
|
export declare function segmentToString(segment: Segment, destination: Destination): string;
|
|
98
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Encodes a sequence of segments as ANSI bytes for `destination`, one segment
|
|
102
|
+
* at a time, over what `sealed` lets reach the wire.
|
|
103
|
+
*/
|
|
99
104
|
export declare function segmentsToString(segments: Iterable<Segment>, destination: Destination): string;
|
|
100
105
|
export declare function renderToString(renderable: Renderable, options?: RenderToStringOptions): string;
|
package/dist/core/render.js
CHANGED
|
@@ -48,6 +48,7 @@ import { ColorDepth, resolveDestination } from "./color.js";
|
|
|
48
48
|
import { Segment } from "./segment.js";
|
|
49
49
|
import { render } from "./protocol.js";
|
|
50
50
|
import { OSC8_CLOSE, osc8Open } from "./osc8.js";
|
|
51
|
+
import { sealed } from "./seal.js";
|
|
51
52
|
const DEFAULT_WIDTH = 80;
|
|
52
53
|
/**
|
|
53
54
|
* `segment` as `destination` shows it: under reduced motion its style
|
|
@@ -64,9 +65,14 @@ export function shownOn(segment, destination) {
|
|
|
64
65
|
}
|
|
65
66
|
/**
|
|
66
67
|
* Encodes one segment as ANSI bytes for `destination`: the reference's
|
|
67
|
-
* `Style.render` for that segment, and nothing for a control segment.
|
|
68
|
+
* `Style.render` for that segment, and nothing for a control segment. It seals
|
|
69
|
+
* the segment alone, and a `logical` row's isolate spans its segments, so a row
|
|
70
|
+
* goes through `segmentsToString` whole.
|
|
68
71
|
*/
|
|
69
72
|
export function segmentToString(segment, destination) {
|
|
73
|
+
return segmentsToString([segment], destination);
|
|
74
|
+
}
|
|
75
|
+
function encoded(segment, destination) {
|
|
70
76
|
if (segment.isControl)
|
|
71
77
|
return "";
|
|
72
78
|
const { text, style } = shownOn(segment, destination);
|
|
@@ -80,11 +86,14 @@ export function segmentToString(segment, destination) {
|
|
|
80
86
|
const link = destination.hyperlinks ? style.link : undefined;
|
|
81
87
|
return link === undefined ? styled : `${osc8Open(link)}${styled}${OSC8_CLOSE}`;
|
|
82
88
|
}
|
|
83
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Encodes a sequence of segments as ANSI bytes for `destination`, one segment
|
|
91
|
+
* at a time, over what `sealed` lets reach the wire.
|
|
92
|
+
*/
|
|
84
93
|
export function segmentsToString(segments, destination) {
|
|
85
94
|
const parts = [];
|
|
86
|
-
for (const segment of segments)
|
|
87
|
-
parts.push(
|
|
95
|
+
for (const segment of sealed(segments, destination.textOrder))
|
|
96
|
+
parts.push(encoded(segment, destination));
|
|
88
97
|
return parts.join("");
|
|
89
98
|
}
|
|
90
99
|
export function renderToString(renderable, options) {
|
|
@@ -92,7 +101,9 @@ export function renderToString(renderable, options) {
|
|
|
92
101
|
// [LAW:dataflow-not-control-flow] An absent spec is truecolor; an explicit
|
|
93
102
|
// `null` stays null (`??` would collapse it).
|
|
94
103
|
// [LAW:single-enforcer] Specs resolve through `resolveDestination`.
|
|
95
|
-
|
|
104
|
+
// Stated, never detected: this call's bytes have no known destination, and
|
|
105
|
+
// its output is deterministic.
|
|
106
|
+
const destinationOptions = { reducedMotion: options?.reducedMotion, textOrder: options?.textOrder ?? "plain" };
|
|
96
107
|
if (options?.env !== undefined)
|
|
97
108
|
destinationOptions.env = options.env;
|
|
98
109
|
if (options?.isTTY !== undefined)
|
|
@@ -114,13 +125,16 @@ export function renderToString(renderable, options) {
|
|
|
114
125
|
asciiOnly: options?.asciiOnly ?? false,
|
|
115
126
|
// [LAW:one-source-of-truth] What is drawn and how it is encoded take one preference.
|
|
116
127
|
reducedMotion: destination.reducedMotion,
|
|
128
|
+
terminal: options?.terminal,
|
|
117
129
|
theme: options?.theme,
|
|
118
130
|
onStyleError: options?.onStyleError,
|
|
119
131
|
highlight: options?.highlight,
|
|
120
132
|
highlighter: options?.highlighter,
|
|
133
|
+
frame: options?.frame,
|
|
121
134
|
// [LAW:one-source-of-truth] The depth the segments below are encoded at,
|
|
122
135
|
// so a renderable measures what this very call will draw.
|
|
123
136
|
colorSystem: destination.colorSystem,
|
|
137
|
+
textOrder: destination.textOrder,
|
|
124
138
|
};
|
|
125
139
|
return segmentsToString(render(renderable, renderOptions), destination);
|
|
126
140
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sealed` — the one total enforcer of what the wire holds.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:single-enforcer] An explicit bidi control reaches the bytes only inside
|
|
5
|
+
* an isolate that accounts for it. The rule has no list of who wrote what: in a
|
|
6
|
+
* segment with no logical stamp every explicit control is deleted, in every
|
|
7
|
+
* order, so a raw `new Segment(text)` is contained and merely shows no effect.
|
|
8
|
+
* What the census withholds goes here too (`wireText`), a segment at a time, on
|
|
9
|
+
* text nothing cuts again, which is why a rule that reads neighbours can live here
|
|
10
|
+
* and not in the sanitizer. A span edge is therefore where that reading stops. A
|
|
11
|
+
* flag cut by one loses its tags and stays a flag; a selector after one is kept;
|
|
12
|
+
* a ZWJ before one is not taken with a deletion past it, so a join can form that
|
|
13
|
+
* `cellLen`, which reads the whole string, did not measure. Segments have always
|
|
14
|
+
* been measured apart and written together, so the same edge in the same text with
|
|
15
|
+
* nothing deleted draws the same row.
|
|
16
|
+
*
|
|
17
|
+
* This runs on the way out, below every cut, because a row is cut after it is
|
|
18
|
+
* drawn: `scroll` divides it, `mergeHorizontal` and a viewport crop it, an
|
|
19
|
+
* overlay splices into it. Controls written at drawing time would be cut with
|
|
20
|
+
* it. `Style.bidi` is a fact on the cells instead, so what survives still names
|
|
21
|
+
* its stamp, and the controls are written around the survivors.
|
|
22
|
+
*/
|
|
23
|
+
import { Segment } from "./segment.js";
|
|
24
|
+
import type { TextOrder } from "./text-order.js";
|
|
25
|
+
/** `segments` as the wire may hold them for `order`. */
|
|
26
|
+
export declare function sealed(segments: Iterable<Segment>, order: TextOrder): Iterable<Segment>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sealed` — the one total enforcer of what the wire holds.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:single-enforcer] An explicit bidi control reaches the bytes only inside
|
|
5
|
+
* an isolate that accounts for it. The rule has no list of who wrote what: in a
|
|
6
|
+
* segment with no logical stamp every explicit control is deleted, in every
|
|
7
|
+
* order, so a raw `new Segment(text)` is contained and merely shows no effect.
|
|
8
|
+
* What the census withholds goes here too (`wireText`), a segment at a time, on
|
|
9
|
+
* text nothing cuts again, which is why a rule that reads neighbours can live here
|
|
10
|
+
* and not in the sanitizer. A span edge is therefore where that reading stops. A
|
|
11
|
+
* flag cut by one loses its tags and stays a flag; a selector after one is kept;
|
|
12
|
+
* a ZWJ before one is not taken with a deletion past it, so a join can form that
|
|
13
|
+
* `cellLen`, which reads the whole string, did not measure. Segments have always
|
|
14
|
+
* been measured apart and written together, so the same edge in the same text with
|
|
15
|
+
* nothing deleted draws the same row.
|
|
16
|
+
*
|
|
17
|
+
* This runs on the way out, below every cut, because a row is cut after it is
|
|
18
|
+
* drawn: `scroll` divides it, `mergeHorizontal` and a viewport crop it, an
|
|
19
|
+
* overlay splices into it. Controls written at drawing time would be cut with
|
|
20
|
+
* it. `Style.bidi` is a fact on the cells instead, so what survives still names
|
|
21
|
+
* its stamp, and the controls are written around the survivors.
|
|
22
|
+
*/
|
|
23
|
+
import { advance, closersOf, opened, withoutExplicit } from "./bidi.js";
|
|
24
|
+
import { wireText } from "./cells.js";
|
|
25
|
+
import { Segment } from "./segment.js";
|
|
26
|
+
const LRI = "\u2066";
|
|
27
|
+
const RLI = "\u2067";
|
|
28
|
+
const PDI = "\u2069";
|
|
29
|
+
/** `text` with closers that match nothing in `open` dropped, so content cannot end the isolate around it. */
|
|
30
|
+
function balanced(text, open) {
|
|
31
|
+
return Array.from(text, (character) => (advance(open, character) ? character : "")).join("");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One row's run of one stamp in its isolate. The initiator is stated from the
|
|
35
|
+
* whole line's direction rather than FSI, so a wrapped row does not re-detect
|
|
36
|
+
* it. Embeddings close by PDF ahead of the PDI because VTE discards a zero-width
|
|
37
|
+
* character at column 0, initiator included, and a PDI matching nothing there
|
|
38
|
+
* closes nothing.
|
|
39
|
+
*/
|
|
40
|
+
function* isolated(run, { level, opens }) {
|
|
41
|
+
const open = opened(opens);
|
|
42
|
+
yield new Segment((level === 1 ? RLI : LRI) + opens);
|
|
43
|
+
for (const segment of run) {
|
|
44
|
+
const text = balanced(segment.text, open);
|
|
45
|
+
yield text === segment.text ? segment : new Segment(text, segment.style, segment.control);
|
|
46
|
+
}
|
|
47
|
+
yield new Segment(closersOf(open.stack.join("")) + PDI);
|
|
48
|
+
}
|
|
49
|
+
/** `segments` as the wire may hold them for `order`. */
|
|
50
|
+
export function* sealed(segments, order) {
|
|
51
|
+
let run = [];
|
|
52
|
+
let stamp;
|
|
53
|
+
const flush = function* () {
|
|
54
|
+
if (stamp !== undefined)
|
|
55
|
+
yield* isolated(run, stamp);
|
|
56
|
+
run = [];
|
|
57
|
+
stamp = undefined;
|
|
58
|
+
};
|
|
59
|
+
for (const raw of segments) {
|
|
60
|
+
const drawn = wireText(raw.text);
|
|
61
|
+
const segment = drawn === raw.text ? raw : new Segment(drawn, raw.style, raw.control);
|
|
62
|
+
const own = order === "logical" && !segment.isControl ? segment.style?.bidi : undefined;
|
|
63
|
+
if (own?.order === "logical") {
|
|
64
|
+
if (own !== stamp)
|
|
65
|
+
yield* flush();
|
|
66
|
+
stamp = own;
|
|
67
|
+
run.push(segment);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
yield* flush();
|
|
71
|
+
const text = withoutExplicit(segment.text);
|
|
72
|
+
yield text === segment.text ? segment : new Segment(text, segment.style, segment.control);
|
|
73
|
+
}
|
|
74
|
+
yield* flush();
|
|
75
|
+
}
|
package/dist/core/segment.d.ts
CHANGED
|
@@ -130,6 +130,14 @@ export declare class Segment {
|
|
|
130
130
|
* short rows padded with spaces, long ones cut.
|
|
131
131
|
*/
|
|
132
132
|
static splitAndCropLines(segments: Iterable<Segment>, width: number): Segment[][];
|
|
133
|
+
/**
|
|
134
|
+
* The plain text of one line of segments, in the coordinate system
|
|
135
|
+
* `divideLine` and `Segment.divide` share: cell offsets into the styled line
|
|
136
|
+
* are cell offsets into this string. [LAW:one-source-of-truth] for that
|
|
137
|
+
* correspondence — the offsets are found in this text and applied to the
|
|
138
|
+
* segments it came from.
|
|
139
|
+
*/
|
|
140
|
+
static plainOf(line: readonly Segment[]): string;
|
|
133
141
|
/**
|
|
134
142
|
* Returns total cell width of a line. Ignores control segments.
|
|
135
143
|
*/
|
|
@@ -177,7 +185,7 @@ export declare class Segment {
|
|
|
177
185
|
* rest every time, so a long line wrapped into many rows took time quadratic
|
|
178
186
|
* in its length.
|
|
179
187
|
*/
|
|
180
|
-
static divide(segments: Segment[], cuts: number[]): Segment[][];
|
|
188
|
+
static divide(segments: readonly Segment[], cuts: number[]): Segment[][];
|
|
181
189
|
/**
|
|
182
190
|
* Pads below to fill height.
|
|
183
191
|
*/
|
package/dist/core/segment.js
CHANGED
|
@@ -307,6 +307,16 @@ export class Segment {
|
|
|
307
307
|
static splitAndCropLines(segments, width) {
|
|
308
308
|
return Segment.splitLines(segments).map((line) => Segment.adjustLineLength(line, width));
|
|
309
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* The plain text of one line of segments, in the coordinate system
|
|
312
|
+
* `divideLine` and `Segment.divide` share: cell offsets into the styled line
|
|
313
|
+
* are cell offsets into this string. [LAW:one-source-of-truth] for that
|
|
314
|
+
* correspondence — the offsets are found in this text and applied to the
|
|
315
|
+
* segments it came from.
|
|
316
|
+
*/
|
|
317
|
+
static plainOf(line) {
|
|
318
|
+
return line.map((segment) => segment.text).join("");
|
|
319
|
+
}
|
|
310
320
|
/**
|
|
311
321
|
* Returns total cell width of a line. Ignores control segments.
|
|
312
322
|
*/
|
|
@@ -426,7 +436,7 @@ export class Segment {
|
|
|
426
436
|
*/
|
|
427
437
|
static divide(segments, cuts) {
|
|
428
438
|
if (cuts.length === 0)
|
|
429
|
-
return [segments];
|
|
439
|
+
return [[...segments]];
|
|
430
440
|
const sections = [[]];
|
|
431
441
|
let offset = 0;
|
|
432
442
|
let next = 0;
|
package/dist/core/style.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { ColorSpec, ColorDepth } from "./color.js";
|
|
5
5
|
import { type Anchor } from "./anchor.js";
|
|
6
|
+
import type { BidiStamp } from "./text-order.js";
|
|
6
7
|
/**
|
|
7
8
|
* Canonical text-attribute inventory. Single source of truth consumed by
|
|
8
9
|
* `Style.parse` / `Style.toString` and the template bindings — adding an
|
|
@@ -45,6 +46,8 @@ export interface StyleOptions {
|
|
|
45
46
|
link?: string;
|
|
46
47
|
meta?: Record<string, unknown>;
|
|
47
48
|
anchor?: Anchor;
|
|
49
|
+
boxFrame?: object;
|
|
50
|
+
bidi?: BidiStamp;
|
|
48
51
|
/**
|
|
49
52
|
* The hold the style's cells are in, both ends or neither: a cell saying
|
|
50
53
|
* only when it next changes would claim, by default, that it always was,
|
|
@@ -58,6 +61,10 @@ export interface Hold {
|
|
|
58
61
|
readonly lastChange: number;
|
|
59
62
|
readonly nextChange: number;
|
|
60
63
|
}
|
|
64
|
+
/** The hold of a cell that never changes: always was, and always will be. */
|
|
65
|
+
export declare const NEVER_CHANGES: Hold;
|
|
66
|
+
/** The moments both `a` and `b` hold through: the later `lastChange` and the sooner `nextChange`. */
|
|
67
|
+
export declare function intersection(a: Hold, b: Hold): Hold;
|
|
61
68
|
export declare class StyleSyntaxError extends Error {
|
|
62
69
|
constructor(message: string, options?: ErrorOptions);
|
|
63
70
|
}
|
|
@@ -85,6 +92,23 @@ export declare class Style {
|
|
|
85
92
|
* unequal, because they came from different places.
|
|
86
93
|
*/
|
|
87
94
|
readonly anchor: Anchor | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* The drawing of a box these cells are the frame of (`Box.frame`), compared
|
|
97
|
+
* by identity. A frame is told by what drew it, never by its glyphs, as a
|
|
98
|
+
* strip's seam is (`JOIN`, `./strip.ts`): a box may draw its frame in blanks,
|
|
99
|
+
* and what it draws in ink an element's own content may draw too. It is a
|
|
100
|
+
* drawing and not a box, so two panels of one `ROUNDED` are two frames, and
|
|
101
|
+
* it is no `Anchor`: a frame's cells say whose they are and not where, since
|
|
102
|
+
* a table draws its top rule before it knows its height. It draws nothing.
|
|
103
|
+
*/
|
|
104
|
+
readonly boxFrame: object | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* What drawing said of these cells' direction, compared by identity, as a
|
|
107
|
+
* link is a fact the encoder turns into bytes: `Segment` never holds a control
|
|
108
|
+
* the library wrote, so no cut of a row can unbalance one, and what survives a
|
|
109
|
+
* cut still names its stamp (`./seal.ts`). It draws nothing.
|
|
110
|
+
*/
|
|
111
|
+
readonly bidi: BidiStamp | undefined;
|
|
88
112
|
/**
|
|
89
113
|
* The hold this style's cells are in, on the timeline of whoever drew the
|
|
90
114
|
* frame: since when what they show has been what it is (`lastChange`), and
|
|
@@ -117,6 +141,10 @@ export declare class Style {
|
|
|
117
141
|
* The same look, drawn from the place `anchor` names, or from nowhere.
|
|
118
142
|
*/
|
|
119
143
|
withAnchor(anchor: Anchor | undefined): Style;
|
|
144
|
+
/** The same look, as the frame of the drawing `boxFrame` names, or of none. */
|
|
145
|
+
withBoxFrame(boxFrame: object | undefined): Style;
|
|
146
|
+
/** The same look, its cells' direction as `bidi` says. */
|
|
147
|
+
withBidi(bidi: BidiStamp | undefined): Style;
|
|
120
148
|
/** The style's text attributes, each as it sets it. */
|
|
121
149
|
get attributes(): Attributes;
|
|
122
150
|
/**
|
|
@@ -163,7 +191,7 @@ export declare class Style {
|
|
|
163
191
|
/**
|
|
164
192
|
* The look as a canonical string that round-trips through `Style.parse()`:
|
|
165
193
|
* colours, attributes and the link, which is all a style string can spell.
|
|
166
|
-
* What a style carries beyond its look — `meta`, `anchor`, `hold` —
|
|
194
|
+
* What a style carries beyond its look — `meta`, `anchor`, `boxFrame`, `bidi`, `hold` —
|
|
167
195
|
* has no spelling and is not here, so two styles that differ only there
|
|
168
196
|
* read alike; `equals` tells them apart.
|
|
169
197
|
*/
|
package/dist/core/style.js
CHANGED
|
@@ -63,7 +63,11 @@ export const ATTRIBUTE_SHORT_ALIASES = {
|
|
|
63
63
|
uu: "underline2",
|
|
64
64
|
};
|
|
65
65
|
/** The hold of a cell that never changes: always was, and always will be. */
|
|
66
|
-
const NEVER_CHANGES = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
|
|
66
|
+
export const NEVER_CHANGES = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
|
|
67
|
+
/** The moments both `a` and `b` hold through: the later `lastChange` and the sooner `nextChange`. */
|
|
68
|
+
export function intersection(a, b) {
|
|
69
|
+
return { lastChange: Math.max(a.lastChange, b.lastChange), nextChange: Math.min(a.nextChange, b.nextChange) };
|
|
70
|
+
}
|
|
67
71
|
export class StyleSyntaxError extends Error {
|
|
68
72
|
constructor(message, options) {
|
|
69
73
|
super(message, options);
|
|
@@ -97,6 +101,23 @@ export class Style {
|
|
|
97
101
|
* unequal, because they came from different places.
|
|
98
102
|
*/
|
|
99
103
|
anchor;
|
|
104
|
+
/**
|
|
105
|
+
* The drawing of a box these cells are the frame of (`Box.frame`), compared
|
|
106
|
+
* by identity. A frame is told by what drew it, never by its glyphs, as a
|
|
107
|
+
* strip's seam is (`JOIN`, `./strip.ts`): a box may draw its frame in blanks,
|
|
108
|
+
* and what it draws in ink an element's own content may draw too. It is a
|
|
109
|
+
* drawing and not a box, so two panels of one `ROUNDED` are two frames, and
|
|
110
|
+
* it is no `Anchor`: a frame's cells say whose they are and not where, since
|
|
111
|
+
* a table draws its top rule before it knows its height. It draws nothing.
|
|
112
|
+
*/
|
|
113
|
+
boxFrame;
|
|
114
|
+
/**
|
|
115
|
+
* What drawing said of these cells' direction, compared by identity, as a
|
|
116
|
+
* link is a fact the encoder turns into bytes: `Segment` never holds a control
|
|
117
|
+
* the library wrote, so no cut of a row can unbalance one, and what survives a
|
|
118
|
+
* cut still names its stamp (`./seal.ts`). It draws nothing.
|
|
119
|
+
*/
|
|
120
|
+
bidi;
|
|
100
121
|
/**
|
|
101
122
|
* The hold this style's cells are in, on the timeline of whoever drew the
|
|
102
123
|
* frame: since when what they show has been what it is (`lastChange`), and
|
|
@@ -142,6 +163,8 @@ export class Style {
|
|
|
142
163
|
this.link = options.link || undefined;
|
|
143
164
|
this.meta = options.meta;
|
|
144
165
|
this.anchor = options.anchor;
|
|
166
|
+
this.boxFrame = options.boxFrame;
|
|
167
|
+
this.bidi = options.bidi;
|
|
145
168
|
}
|
|
146
169
|
get isNull() {
|
|
147
170
|
return (this.color === undefined &&
|
|
@@ -162,6 +185,8 @@ export class Style {
|
|
|
162
185
|
this.link === undefined &&
|
|
163
186
|
this.meta === undefined &&
|
|
164
187
|
this.anchor === undefined &&
|
|
188
|
+
this.boxFrame === undefined &&
|
|
189
|
+
this.bidi === undefined &&
|
|
165
190
|
this.hold.nextChange === Number.POSITIVE_INFINITY &&
|
|
166
191
|
this.hold.lastChange === Number.NEGATIVE_INFINITY);
|
|
167
192
|
}
|
|
@@ -199,6 +224,14 @@ export class Style {
|
|
|
199
224
|
withAnchor(anchor) {
|
|
200
225
|
return this.with({ anchor });
|
|
201
226
|
}
|
|
227
|
+
/** The same look, as the frame of the drawing `boxFrame` names, or of none. */
|
|
228
|
+
withBoxFrame(boxFrame) {
|
|
229
|
+
return this.with({ boxFrame });
|
|
230
|
+
}
|
|
231
|
+
/** The same look, its cells' direction as `bidi` says. */
|
|
232
|
+
withBidi(bidi) {
|
|
233
|
+
return this.with({ bidi });
|
|
234
|
+
}
|
|
202
235
|
/** The style's text attributes, each as it sets it. */
|
|
203
236
|
get attributes() {
|
|
204
237
|
// [LAW:one-source-of-truth] Keyed by `ATTRIBUTE_NAMES`, whose every name is a key here: the cast is that list's own claim.
|
|
@@ -257,9 +290,10 @@ export class Style {
|
|
|
257
290
|
? { ...this.meta, ...other.meta }
|
|
258
291
|
: other.meta ?? this.meta,
|
|
259
292
|
anchor: other.anchor ?? this.anchor,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
boxFrame: other.boxFrame ?? this.boxFrame,
|
|
294
|
+
bidi: other.bidi ?? this.bidi,
|
|
295
|
+
// Cells under both change when either does.
|
|
296
|
+
hold: intersection(this.hold, other.hold),
|
|
263
297
|
});
|
|
264
298
|
}
|
|
265
299
|
equals(other) {
|
|
@@ -282,6 +316,8 @@ export class Style {
|
|
|
282
316
|
this.overline === other.overline &&
|
|
283
317
|
this.link === other.link &&
|
|
284
318
|
anchorsEqual(this.anchor, other.anchor) &&
|
|
319
|
+
this.boxFrame === other.boxFrame &&
|
|
320
|
+
this.bidi === other.bidi &&
|
|
285
321
|
this.hold.nextChange === other.hold.nextChange &&
|
|
286
322
|
this.hold.lastChange === other.hold.lastChange);
|
|
287
323
|
}
|
|
@@ -333,7 +369,7 @@ export class Style {
|
|
|
333
369
|
/**
|
|
334
370
|
* The look as a canonical string that round-trips through `Style.parse()`:
|
|
335
371
|
* colours, attributes and the link, which is all a style string can spell.
|
|
336
|
-
* What a style carries beyond its look — `meta`, `anchor`, `hold` —
|
|
372
|
+
* What a style carries beyond its look — `meta`, `anchor`, `boxFrame`, `bidi`, `hold` —
|
|
337
373
|
* has no spelling and is not here, so two styles that differ only there
|
|
338
374
|
* read alike; `equals` tells them apart.
|
|
339
375
|
*/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a destination does with text direction.
|
|
3
|
+
*
|
|
4
|
+
* Every order above `plain` is a claim about the terminal, and a wrong claim
|
|
5
|
+
* costs more than none. Visual order on a terminal that also reorders reverses
|
|
6
|
+
* the text twice, and no query answers for every such terminal. An isolate on a
|
|
7
|
+
* terminal whose width table predates it advances the cursor (xterm.js 6 does),
|
|
8
|
+
* which moves a table's border and the rows a painter goes back over. So what
|
|
9
|
+
* nothing is known about gets bare text, and `detectTextOrder` grants more only
|
|
10
|
+
* on evidence.
|
|
11
|
+
*/
|
|
12
|
+
import { type Env } from "./env.js";
|
|
13
|
+
/**
|
|
14
|
+
* - `plain`: nothing is known of the destination, or it is not a display: a
|
|
15
|
+
* pipe, a file, an unknown terminal. Characters only, in logical order, with
|
|
16
|
+
* no explicit bidi control.
|
|
17
|
+
* - `logical`: a display known to reorder and to hold the controls at zero
|
|
18
|
+
* width. Rows are sealed in isolates.
|
|
19
|
+
* - `visual`: a display known not to reorder. Rows are written reordered and
|
|
20
|
+
* shaped, with no controls.
|
|
21
|
+
*/
|
|
22
|
+
export type TextOrder = "plain" | "logical" | "visual";
|
|
23
|
+
/** A row's paragraph direction: 0 left to right, 1 right to left. */
|
|
24
|
+
export type ParagraphLevel = 0 | 1;
|
|
25
|
+
/** Cells written for a terminal that reorders them: their row's direction, and what is still open where they begin. */
|
|
26
|
+
export interface LogicalStamp {
|
|
27
|
+
readonly order: "logical";
|
|
28
|
+
readonly level: ParagraphLevel;
|
|
29
|
+
readonly opens: string;
|
|
30
|
+
}
|
|
31
|
+
/** Cells already in visual order, for an export not to reverse again. */
|
|
32
|
+
export interface VisualStamp {
|
|
33
|
+
readonly order: "visual";
|
|
34
|
+
}
|
|
35
|
+
export type BidiStamp = LogicalStamp | VisualStamp;
|
|
36
|
+
/**
|
|
37
|
+
* The order `env` gives evidence for.
|
|
38
|
+
*
|
|
39
|
+
* An environment is inherited: `code .` from Windows Terminal carries
|
|
40
|
+
* `WT_SESSION` into VS Code's xterm.js terminal, and Alacritty clears nothing
|
|
41
|
+
* from GNOME Terminal. `TERM` is the one variable every terminal writes for its
|
|
42
|
+
* own child, so it is read first, and a variable a parent could have left counts
|
|
43
|
+
* only beside the `TERM` its own terminal sets. The rows are in the order that
|
|
44
|
+
* makes that true; a terminal naming itself off this list is `plain`.
|
|
45
|
+
*
|
|
46
|
+
* Not listed, each for a reason: xterm.js lays a row out through the browser and
|
|
47
|
+
* advances for an isolate, and VS Code is xterm.js. WezTerm's bidi is a setting
|
|
48
|
+
* the environment cannot show. iTerm2, Konsole, Terminal.app, mintty and mlterm
|
|
49
|
+
* are unmeasured. Any program may state its order.
|
|
50
|
+
*/
|
|
51
|
+
export declare function detectTextOrder(env: Env, isTTY: boolean): TextOrder;
|