@promptctl/rich-js 0.27.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 +36 -17
- package/dist/core/animation.js +61 -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 +6 -7
- package/dist/core/color.js +11 -13
- package/dist/core/console.d.ts +18 -1
- package/dist/core/console.js +24 -5
- package/dist/core/control-codes.d.ts +61 -0
- package/dist/core/control-codes.js +157 -0
- 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/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 +27 -6
- package/dist/core/protocol.js +14 -6
- package/dist/core/render.d.ts +8 -3
- package/dist/core/render.js +18 -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 +1 -1
- package/dist/core/view.js +5 -4
- package/dist/core/wrap.d.ts +24 -6
- package/dist/core/wrap.js +82 -16
- package/dist/host/app.d.ts +44 -3
- package/dist/host/app.js +111 -6
- 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 +5 -4
- 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 +60 -59
- package/dist/renderables/effect.js +91 -81
- package/dist/renderables/effects.d.ts +37 -8
- package/dist/renderables/effects.js +185 -52
- 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 +5 -7
- package/dist/renderables/lifecycle.js +6 -7
- package/dist/renderables/markdown.js +4 -2
- package/dist/renderables/panel.js +6 -4
- 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 +3 -2
- package/dist/themes/buildPalette.js +2 -2
- package/dist/themes/data/index.js +1 -4
- package/dist/themes/data/textual-ansi.js +0 -143
- package/dist/themes/ramp.d.ts +1 -1
- package/dist/themes/ramp.js +1 -1
- package/dist/themes/registry.d.ts +6 -8
- package/dist/themes/registry.js +2 -4
- package/dist/themes/terminalThemes.d.ts +4 -11
- package/dist/themes/terminalThemes.js +31 -47
- 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/themed-widget.js +3 -3
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +5 -5
- package/package.json +5 -4
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orienting a drawn line for its destination's `TextOrder`.
|
|
3
|
+
*
|
|
4
|
+
* Levels are resolved over the whole logical line and a row is ordered alone,
|
|
5
|
+
* because UAX #9 runs L1 and L2 after line breaking. So `directed` runs ahead of
|
|
6
|
+
* wrapping and each row's `orient` after it, and cuts fall on logical offsets in
|
|
7
|
+
* every order: what survives a cut is a logical prefix of the sentence, which is
|
|
8
|
+
* what a reader of either direction expects.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:dataflow-not-control-flow] The order chooses values and never steps: a
|
|
11
|
+
* `plain` line passes through both.
|
|
12
|
+
*/
|
|
13
|
+
import { isBidi, lineLevels, mirrored, openersAlong, paragraph, paragraphLevel, REMOVED, shaped, visualOrder, withoutExplicit, withoutMarks } from "./bidi.js";
|
|
14
|
+
import { cellLen, graphemes } from "./cells.js";
|
|
15
|
+
import { pattern } from "./pattern.js";
|
|
16
|
+
import { Segment } from "./segment.js";
|
|
17
|
+
import { NULL_STYLE } from "./style.js";
|
|
18
|
+
const TRAILING_WHITESPACE_RE = pattern(/\s+$/);
|
|
19
|
+
// From the front, greedily: anchored at the end, a scan restarts at each format
|
|
20
|
+
// character of a run, and that is quadratic on content anyone can send.
|
|
21
|
+
const THROUGH_CONTENT = pattern(/^[^]*[^\s\p{Cf}]/u);
|
|
22
|
+
/**
|
|
23
|
+
* The cells that end `text` and are no content: its whitespace, once what draws no
|
|
24
|
+
* cell among it is gone. A mark drawn on a space is not in it, because that space
|
|
25
|
+
* is a cell with something on it.
|
|
26
|
+
*/
|
|
27
|
+
export function drawnTail(text) {
|
|
28
|
+
const tail = text.slice(THROUGH_CONTENT.exec(text)?.[0].length ?? 0);
|
|
29
|
+
return Array.from(tail).filter((character) => cellLen(character) > 0).join("");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The cells of trailing whitespace on a wrapped line.
|
|
33
|
+
*
|
|
34
|
+
* A wrap cuts before a word, so the line it closes ends with the whitespace
|
|
35
|
+
* that followed *its* last word — padding the break created, not content the
|
|
36
|
+
* author wrote. Measuring it is what lets the overflow method tell "this line
|
|
37
|
+
* was cut" from "this line ends in spaces" and stamp an ellipsis only on the
|
|
38
|
+
* first, and what keeps a centred line from drifting half a space off true.
|
|
39
|
+
*
|
|
40
|
+
* Asked of the line's text rather than walked back through its segments,
|
|
41
|
+
* because where a line ends is a fact about the characters and not about how
|
|
42
|
+
* they were split. Walking the segments made it a fact about both: it read a
|
|
43
|
+
* segment with no trailing whitespace as content and stopped there, which an
|
|
44
|
+
* empty segment also looks like — and a crop leaves one behind. A styled line
|
|
45
|
+
* whose last cells were cropped therefore reported no hanging whitespace at
|
|
46
|
+
* all and was centred as though it were content to the edge, so a span landing
|
|
47
|
+
* anywhere in a wrap's whitespace un-centred the line it closed. What draws no
|
|
48
|
+
* cell after the whitespace is no content either: `closed` writes a bidi control
|
|
49
|
+
* exactly there, and a cut leaves a zero-width code point that opened the next
|
|
50
|
+
* word on the row before it.
|
|
51
|
+
*/
|
|
52
|
+
export function hangingWhitespace(line) {
|
|
53
|
+
return cellLen(TRAILING_WHITESPACE_RE.exec(drawnTail(Segment.plainOf(line)))?.[0] ?? "");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A row ready to order: one a wrap closed loses the whitespace it left hanging,
|
|
57
|
+
* which rule L1 would otherwise stand at a right-to-left row's left edge. The
|
|
58
|
+
* last row keeps what the author wrote.
|
|
59
|
+
*/
|
|
60
|
+
function rowOf(shown, last) {
|
|
61
|
+
const row = [...shown];
|
|
62
|
+
return last ? row : Segment.adjustLineLength(row, Segment.getLineLength(row) - hangingWhitespace(row), undefined, false);
|
|
63
|
+
}
|
|
64
|
+
/** `line` with `keep` applied to each segment's text. */
|
|
65
|
+
function rewritten(line, keep) {
|
|
66
|
+
return line.map((segment) => {
|
|
67
|
+
const text = keep(segment.text);
|
|
68
|
+
return text === segment.text ? segment : new Segment(text, segment.style, segment.control);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const UNTOUCHED = (line) => ({ line, oriented: false, orient: (shown) => [...shown] });
|
|
72
|
+
const VISUAL_STAMP = { order: "visual" };
|
|
73
|
+
function withStamp(line, bidi) {
|
|
74
|
+
return line.map((segment) => new Segment(segment.text, (segment.style ?? NULL_STYLE).withBidi(bidi), segment.control));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `levels` of `source` carried onto `shown`, which `source` became by losing a
|
|
78
|
+
* tail and gaining a marker or wider gaps: each character that survived keeps
|
|
79
|
+
* its level, and one that did not exist takes the level before it, the
|
|
80
|
+
* paragraph's when nothing precedes.
|
|
81
|
+
*/
|
|
82
|
+
function alignedLevels(shown, source, levels, paragraphLevel) {
|
|
83
|
+
const out = [];
|
|
84
|
+
let at = 0;
|
|
85
|
+
for (let unit = 0; unit < shown.length; unit += 1) {
|
|
86
|
+
if (shown[unit] === source[at]) {
|
|
87
|
+
out.push(levels[at]);
|
|
88
|
+
at += 1;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
out.push(at === source.length || at === 0 ? paragraphLevel : levels[at - 1]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** L1, L2 and L4 on a row: its grapheme clusters in display order, each carrying its style. */
|
|
97
|
+
function displayed(shown, levels, paragraphLevel) {
|
|
98
|
+
const text = Segment.plainOf(shown);
|
|
99
|
+
const finished = lineLevels(text, levels, paragraphLevel);
|
|
100
|
+
// Clusters are found in the whole row, because a span edge inside one is no
|
|
101
|
+
// place to cut it: reversed apart, a mark lands on another letter.
|
|
102
|
+
const styles = shown.flatMap((segment) => new Array(segment.text.length).fill(segment.style));
|
|
103
|
+
const clusters = [];
|
|
104
|
+
let unit = 0;
|
|
105
|
+
let previous = paragraphLevel;
|
|
106
|
+
for (const cluster of graphemes(text)) {
|
|
107
|
+
// A cluster of what X9 removes holds no level of its own, and stays where the text put it.
|
|
108
|
+
const level = finished[unit] === REMOVED ? previous : finished[unit];
|
|
109
|
+
previous = level;
|
|
110
|
+
clusters.push({ text: cluster, style: styles[unit], level });
|
|
111
|
+
unit += cluster.length;
|
|
112
|
+
}
|
|
113
|
+
const forms = shaped(clusters.map((cluster) => cluster.text));
|
|
114
|
+
const order = visualOrder(clusters.map((cluster) => cluster.level));
|
|
115
|
+
const out = [];
|
|
116
|
+
let run = "";
|
|
117
|
+
let style;
|
|
118
|
+
const flush = () => {
|
|
119
|
+
if (run !== "")
|
|
120
|
+
out.push(new Segment(run, style));
|
|
121
|
+
run = "";
|
|
122
|
+
};
|
|
123
|
+
for (const index of order) {
|
|
124
|
+
const cluster = clusters[index];
|
|
125
|
+
if (cluster.style !== style)
|
|
126
|
+
flush();
|
|
127
|
+
style = cluster.style;
|
|
128
|
+
run += cluster.level & 1 ? mirroredCluster(forms[index]) : forms[index];
|
|
129
|
+
}
|
|
130
|
+
flush();
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function mirroredCluster(cluster) {
|
|
134
|
+
const base = cluster.codePointAt(0);
|
|
135
|
+
return String.fromCodePoint(mirrored(base)) + cluster.slice(base > 0xffff ? 2 : 1);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* What `order` takes out of a line ahead of wrapping, and so out of its
|
|
139
|
+
* measure: what will not reach the wire. Under `visual` that is X9's own step
|
|
140
|
+
* and the marks with it: they have done their work once levels are resolved, and
|
|
141
|
+
* L2 would move a trailing one to a row's first cell, where xterm.js advances
|
|
142
|
+
* for it.
|
|
143
|
+
*/
|
|
144
|
+
const REMOVALS = {
|
|
145
|
+
plain: withoutExplicit,
|
|
146
|
+
logical: (text) => text,
|
|
147
|
+
visual: (text) => withoutMarks(withoutExplicit(text)),
|
|
148
|
+
};
|
|
149
|
+
/** What `order` does with `line`, one logical line of drawn segments. */
|
|
150
|
+
export function directed(line, order) {
|
|
151
|
+
if (!isBidi(Segment.plainOf(line)))
|
|
152
|
+
return UNTOUCHED(line);
|
|
153
|
+
if (order === "plain")
|
|
154
|
+
return UNTOUCHED(rewritten(line, REMOVALS.plain));
|
|
155
|
+
const text = Segment.plainOf(line);
|
|
156
|
+
if (order === "logical") {
|
|
157
|
+
const level = paragraphLevel(text);
|
|
158
|
+
const openersAt = openersAlong(text);
|
|
159
|
+
return {
|
|
160
|
+
line,
|
|
161
|
+
oriented: true,
|
|
162
|
+
orient: (shown, start, source) => withStamp(rowOf(shown, start + source.length === text.length), { order: "logical", level, opens: openersAt(start) }),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const { level, levels } = paragraph(text);
|
|
166
|
+
const consumed = rewritten(line, REMOVALS.visual);
|
|
167
|
+
const kept = Segment.plainOf(consumed);
|
|
168
|
+
const keptLevels = [];
|
|
169
|
+
let at = 0;
|
|
170
|
+
for (let unit = 0; unit < text.length; unit += 1) {
|
|
171
|
+
if (text[unit] === kept[at]) {
|
|
172
|
+
keptLevels.push(levels[unit]);
|
|
173
|
+
at += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
line: consumed,
|
|
178
|
+
oriented: true,
|
|
179
|
+
orient: (shown, start, source) => {
|
|
180
|
+
const row = rowOf(shown, start + source.length === kept.length);
|
|
181
|
+
const own = keptLevels.slice(start, start + source.length);
|
|
182
|
+
return withStamp(displayed(row, alignedLevels(Segment.plainOf(row), source, own, level), level), VISUAL_STAMP);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** `text` as `order` measures it: what `directed` removes ahead of wrapping is no word. */
|
|
187
|
+
export function measured(text, order) {
|
|
188
|
+
return isBidi(text) ? REMOVALS[order](text) : text;
|
|
189
|
+
}
|
|
190
|
+
/** The offsets in `text` of the code points that take a cell: a tab does, once expanded. */
|
|
191
|
+
function drawnOffsets(text) {
|
|
192
|
+
const drawn = new Set();
|
|
193
|
+
let unit = 0;
|
|
194
|
+
for (const character of text) {
|
|
195
|
+
if (character === "\t" || cellLen(character) > 0)
|
|
196
|
+
drawn.add(unit);
|
|
197
|
+
unit += character.length;
|
|
198
|
+
}
|
|
199
|
+
return drawn;
|
|
200
|
+
}
|
|
201
|
+
/** The offsets in `text`, one row, of the characters `order` shows at its left and right edges. */
|
|
202
|
+
export function edges(text, order) {
|
|
203
|
+
const units = order === "plain" || !isBidi(text) ? Array.from({ length: text.length }, (_, unit) => unit) : ordered(text);
|
|
204
|
+
const offsets = drawnOffsets(text);
|
|
205
|
+
const drawn = units.filter((unit) => offsets.has(unit));
|
|
206
|
+
return { left: drawn[0] ?? 0, right: drawn.at(-1) ?? 0 };
|
|
207
|
+
}
|
|
208
|
+
function ordered(text) {
|
|
209
|
+
const { level, levels } = paragraph(text);
|
|
210
|
+
return visualOrder(lineLevels(text, levels, level));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* A label set between `before` and `after`, which are frame and stay where they
|
|
214
|
+
* are, for a row drawn with no `RichText`: only the label is oriented, or a
|
|
215
|
+
* button's brackets would be mirrored around it. A label `directed` leaves alone
|
|
216
|
+
* is one segment with its frame, as it always was.
|
|
217
|
+
*/
|
|
218
|
+
export function labelled(before, label, after, style, order) {
|
|
219
|
+
const { line, oriented, orient } = directed([new Segment(label, style)], order ?? "plain");
|
|
220
|
+
const text = Segment.plainOf(line);
|
|
221
|
+
if (!oriented)
|
|
222
|
+
return [new Segment(before + text + after, style)];
|
|
223
|
+
return [new Segment(before, style), ...orient(line, 0, text), new Segment(after, style)].filter((segment) => segment.text !== "");
|
|
224
|
+
}
|
package/dist/core/osc8.d.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* every link reaches the wire through) opens a link with `osc8Open` and closes
|
|
7
7
|
* it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
|
|
8
8
|
* test extracting URLs) matches them with `OSC8`; the data-model boundary
|
|
9
|
-
* (RichText) cleans URLs with `
|
|
10
|
-
* terminator set below,
|
|
11
|
-
*
|
|
9
|
+
* (RichText) cleans URLs with `stripUrlControls`. Producer and reader share the
|
|
10
|
+
* terminator set below, and the sanitizer removes all of it, so no URL it has
|
|
11
|
+
* cleaned can end a wrap early.
|
|
12
12
|
*
|
|
13
13
|
* [LAW:locality-or-seam] This module depends only on `fnv1a`; every consumer
|
|
14
14
|
* imports downward.
|
|
@@ -30,8 +30,15 @@
|
|
|
30
30
|
* different URIs merges nothing — the URIs still differ — so a 32-bit hash is
|
|
31
31
|
* exact, not approximate.
|
|
32
32
|
*/
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `url` without a control, a line separator or a bidi control. RFC 3986 admits no
|
|
35
|
+
* control and RFC 3987 no bidi control: ESC, BEL and ST end the wrap, CAN and SUB
|
|
36
|
+
* abort it, U+009B opens a sequence inside it, and U+202E would reorder the address
|
|
37
|
+
* on hover. Not the whole census, because an address can hold the rest: ZWNJ is
|
|
38
|
+
* part of a Persian word, and taking it would send the link somewhere else.
|
|
39
|
+
* `sealed` reads a segment's text, so this is the one rule for a link.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stripUrlControls(url: string): string;
|
|
35
42
|
/**
|
|
36
43
|
* The bytes that open a hyperlink to `url`.
|
|
37
44
|
*
|
package/dist/core/osc8.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* every link reaches the wire through) opens a link with `osc8Open` and closes
|
|
7
7
|
* it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
|
|
8
8
|
* test extracting URLs) matches them with `OSC8`; the data-model boundary
|
|
9
|
-
* (RichText) cleans URLs with `
|
|
10
|
-
* terminator set below,
|
|
11
|
-
*
|
|
9
|
+
* (RichText) cleans URLs with `stripUrlControls`. Producer and reader share the
|
|
10
|
+
* terminator set below, and the sanitizer removes all of it, so no URL it has
|
|
11
|
+
* cleaned can end a wrap early.
|
|
12
12
|
*
|
|
13
13
|
* [LAW:locality-or-seam] This module depends only on `fnv1a`; every consumer
|
|
14
14
|
* imports downward.
|
|
@@ -37,9 +37,16 @@ import { pattern } from "./pattern.js";
|
|
|
37
37
|
* begins ST `ESC \`), BEL (`\x07`), and the 8-bit ST (`\x9c`).
|
|
38
38
|
*/
|
|
39
39
|
const TERMINATORS = "\\x1b\\x07\\x9c";
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `url` without a control, a line separator or a bidi control. RFC 3986 admits no
|
|
42
|
+
* control and RFC 3987 no bidi control: ESC, BEL and ST end the wrap, CAN and SUB
|
|
43
|
+
* abort it, U+009B opens a sequence inside it, and U+202E would reorder the address
|
|
44
|
+
* on hover. Not the whole census, because an address can hold the rest: ZWNJ is
|
|
45
|
+
* part of a Persian word, and taking it would send the link somewhere else.
|
|
46
|
+
* `sealed` reads a segment's text, so this is the one rule for a link.
|
|
47
|
+
*/
|
|
48
|
+
export function stripUrlControls(url) {
|
|
49
|
+
return url.replace(/[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]/gu, "");
|
|
43
50
|
}
|
|
44
51
|
/**
|
|
45
52
|
* The bytes that open a hyperlink to `url`.
|
|
@@ -50,7 +57,7 @@ export function stripOscTerminators(url) {
|
|
|
50
57
|
* built directly with `new Style({ link })` reaches here unsanitized).
|
|
51
58
|
*/
|
|
52
59
|
export function osc8Open(url) {
|
|
53
|
-
const clean =
|
|
60
|
+
const clean = stripUrlControls(url);
|
|
54
61
|
// Hex digits are a legal `id=` value: no `:` or `;`.
|
|
55
62
|
return `\x1b]8;id=${fnv1a(clean)};${clean}\x1b\\`;
|
|
56
63
|
}
|
package/dist/core/pretty.js
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* once to probe it and again to place it, so a node at depth d was visited 2^d
|
|
17
17
|
* times.
|
|
18
18
|
*/
|
|
19
|
-
import { asCellCol, cellColToCodeUnitOffset, cellLen, graphemes, splitAtCells } from "./cells.js";
|
|
19
|
+
import { asCellCol, cellColToCodeUnitOffset, cellLen, graphemes, replaceWithheld, splitAtCells, unicodeEscape, wireText } from "./cells.js";
|
|
20
20
|
import { divideLine } from "./wrap.js";
|
|
21
21
|
import { Memo } from "./memo.js";
|
|
22
22
|
import { Segment } from "./segment.js";
|
|
23
|
-
import {
|
|
23
|
+
import { closed } from "./bidi.js";
|
|
24
|
+
import { RichText, sanitized, stripControlChars } from "./text.js";
|
|
24
25
|
import { ReprHighlighter } from "./highlighter.js";
|
|
25
26
|
import { drawable, render } from "./protocol.js";
|
|
26
27
|
import { pattern } from "./pattern.js";
|
|
@@ -66,6 +67,12 @@ function lineColumn(column, text) {
|
|
|
66
67
|
const lastNewline = text.lastIndexOf("\n");
|
|
67
68
|
return lastNewline === -1 ? column + cellLen(text) : cellLen(text.slice(lastNewline + 1));
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Text a value wrote itself, as layout reads it: what the wire will hold, every
|
|
72
|
+
* line ending an LF, its direction closed. A soft hyphen is gone with the rest,
|
|
73
|
+
* because this text is on show and the rows it hangs on are cut here.
|
|
74
|
+
*/
|
|
75
|
+
const spelled = (text) => closed(wireText(sanitized(text)));
|
|
69
76
|
/** Text with no line break in it. */
|
|
70
77
|
const flat = (text) => ({ text, margins: [], arrows: [] });
|
|
71
78
|
/** A new line whose first `margin` cells are the structure's indent. */
|
|
@@ -122,7 +129,7 @@ function follow(out, laid, tail) {
|
|
|
122
129
|
* the same pass runs either way.
|
|
123
130
|
*
|
|
124
131
|
* The guide offsets are counted over the text `RichText` will hold, which has
|
|
125
|
-
*
|
|
132
|
+
* every `strip` code point deleted; counted over the raw text, every guide past
|
|
126
133
|
* a `\r` in a key or a `toString` would land one cell right. A margin is only
|
|
127
134
|
* ever spaces, so stripping cannot move one. An arrow is moved by the
|
|
128
135
|
* stripping of what precedes it, and by the blank lines dropped before it;
|
|
@@ -181,7 +188,7 @@ function drawMargins(lines, indent, guide) {
|
|
|
181
188
|
function rowsOf(line, stay, hangRoom) {
|
|
182
189
|
const lead = line.slice(0, line.length - line.trimStart().length);
|
|
183
190
|
const rest = line.slice(stay);
|
|
184
|
-
const hung = splitAtCells(rest, divideLine(rest, asCellCol(hangRoom - cellLen(lead)), { fold: true }))
|
|
191
|
+
const hung = splitAtCells(rest, divideLine(rest, asCellCol(hangRoom - cellLen(lead)), { fold: true }).map(({ cell }) => cell))
|
|
185
192
|
.map((row) => row.trim())
|
|
186
193
|
.filter((row) => row !== "")
|
|
187
194
|
.map((row) => lead + row);
|
|
@@ -201,7 +208,7 @@ function stayingLength(line, room, canHang) {
|
|
|
201
208
|
return line.length;
|
|
202
209
|
const cut = (fold) => {
|
|
203
210
|
const first = divideLine(line, asCellCol(room), { fold })[0];
|
|
204
|
-
return first === undefined ? line.length : cellColToCodeUnitOffset(line, first);
|
|
211
|
+
return first === undefined ? line.length : cellColToCodeUnitOffset(line, first.cell);
|
|
205
212
|
};
|
|
206
213
|
const words = cut(false);
|
|
207
214
|
if (cellLen(line.slice(0, words).trimEnd()) <= room)
|
|
@@ -343,7 +350,7 @@ function take(source, limit) {
|
|
|
343
350
|
function threw(error) {
|
|
344
351
|
// An `Error` from any realm carries its `message` as a string.
|
|
345
352
|
const message = error?.message;
|
|
346
|
-
return `[Threw: ${typeof message === "string" ? message : String(error)}]`;
|
|
353
|
+
return `[Threw: ${spelled(typeof message === "string" ? message : String(error))}]`;
|
|
347
354
|
}
|
|
348
355
|
/**
|
|
349
356
|
* Does this value carry its own string form, or must we reflect on its keys?
|
|
@@ -459,16 +466,13 @@ const BARE_KEY = pattern(/^(?![^]*\p{Default_Ignorable_Code_Point})(?:[\p{ID_Sta
|
|
|
459
466
|
/**
|
|
460
467
|
* What Python's `str.isprintable` refuses \u2014 control, format, private-use,
|
|
461
468
|
* unassigned, and every separator but the space \u2014 which Rich's repr escapes
|
|
462
|
-
* and JSON.stringify leaves raw past C0. Raw,
|
|
463
|
-
*
|
|
464
|
-
* reorders what is around it.
|
|
469
|
+
* and JSON.stringify leaves raw past C0. Raw, a C0 control or DEL is a picture in
|
|
470
|
+
* `RichText`, a C1 control a stand-in and NEL a line break, and a format character
|
|
471
|
+
* hides or reorders what is around it: escaped, the repr shows which one it was.
|
|
465
472
|
*/
|
|
466
473
|
const unprintable = () => /(?! )[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/gu;
|
|
467
|
-
/** `value` in double quotes, every character that does not print escaped. */
|
|
468
|
-
const quoted = (value) => JSON.stringify(value).replace(unprintable(),
|
|
469
|
-
const hex = c.codePointAt(0).toString(16).padStart(4, "0");
|
|
470
|
-
return hex.length > 4 ? `\\u{${hex}}` : `\\u${hex}`;
|
|
471
|
-
});
|
|
474
|
+
/** `value` in double quotes, every character that does not print or that the wire would withhold escaped. */
|
|
475
|
+
const quoted = (value) => replaceWithheld(JSON.stringify(value), unicodeEscape).replace(unprintable(), unicodeEscape);
|
|
472
476
|
export class Pretty {
|
|
473
477
|
data;
|
|
474
478
|
indent;
|
|
@@ -549,9 +553,9 @@ export class Pretty {
|
|
|
549
553
|
case "boolean":
|
|
550
554
|
return String(value);
|
|
551
555
|
case "symbol":
|
|
552
|
-
return value.toString();
|
|
556
|
+
return spelled(value.toString());
|
|
553
557
|
case "function":
|
|
554
|
-
return `[Function: ${value.name || "anonymous"}]`;
|
|
558
|
+
return `[Function: ${spelled(String(value.name || "anonymous"))}]`;
|
|
555
559
|
}
|
|
556
560
|
return null;
|
|
557
561
|
}
|
|
@@ -667,9 +671,9 @@ export class Pretty {
|
|
|
667
671
|
holes: [{ read: () => v, tail: flat("") }],
|
|
668
672
|
})));
|
|
669
673
|
case "opaque":
|
|
670
|
-
return { kind: "text", text: form.text };
|
|
674
|
+
return { kind: "text", text: spelled(form.text) };
|
|
671
675
|
case "self":
|
|
672
|
-
return { kind: "text", text: String(value) };
|
|
676
|
+
return { kind: "text", text: spelled(String(value)) };
|
|
673
677
|
case "keys": {
|
|
674
678
|
// No self-description, so the keys are the whole story.
|
|
675
679
|
const { record } = form;
|
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.
|
|
@@ -97,6 +98,17 @@ export interface RenderOptions {
|
|
|
97
98
|
* handed. Absent means truecolor.
|
|
98
99
|
*/
|
|
99
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;
|
|
100
112
|
/**
|
|
101
113
|
* The names a style string may use. A `Console` passes its own; absent, the
|
|
102
114
|
* built-in defaults apply. Read it through `getStyle` rather than directly.
|
|
@@ -111,11 +123,11 @@ export interface RenderOptions {
|
|
|
111
123
|
onStyleError?: StyleErrorHandler;
|
|
112
124
|
/**
|
|
113
125
|
* The reader asked for reduced motion — CSS's `prefers-reduced-motion`,
|
|
114
|
-
* which a terminal has no way to report, so a host is told.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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.
|
|
119
131
|
*/
|
|
120
132
|
reducedMotion?: boolean;
|
|
121
133
|
/**
|
|
@@ -148,6 +160,15 @@ export interface RenderOptions {
|
|
|
148
160
|
* decides the frame's layout, so when it changes, the frame may.
|
|
149
161
|
*/
|
|
150
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;
|
|
151
172
|
}
|
|
152
173
|
/** The one field of the render options that says whether the reader wants motion. */
|
|
153
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" | "frame"> {
|
|
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,6 +125,7 @@ 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,
|
|
@@ -122,6 +134,7 @@ export function renderToString(renderable, options) {
|
|
|
122
134
|
// [LAW:one-source-of-truth] The depth the segments below are encoded at,
|
|
123
135
|
// so a renderable measures what this very call will draw.
|
|
124
136
|
colorSystem: destination.colorSystem,
|
|
137
|
+
textOrder: destination.textOrder,
|
|
125
138
|
};
|
|
126
139
|
return segmentsToString(render(renderable, renderOptions), destination);
|
|
127
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>;
|