@promptctl/rich-js 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state — `State<T>`, the one place a renderable keeps state that changes after
|
|
3
|
+
* it is built, and the refusal of a write to it while something is drawn.
|
|
4
|
+
*
|
|
5
|
+
* A render changes nothing. React refuses a state update during render because
|
|
6
|
+
* state changes only through a setter, and the setter knows the render phase;
|
|
7
|
+
* this is that setter. `set` throws while a render or measure is in progress
|
|
8
|
+
* (`activePass`, owned by `render` and `measure` in `./protocol.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Ownership is React's line too: a state made during a pass belongs to that
|
|
11
|
+
* pass, and the pass may set it. A render that builds a fresh value and fills
|
|
12
|
+
* it as it draws — a `Table` it adds rows to — changes nothing anyone held
|
|
13
|
+
* before it began. Only a state that existed before the pass is refused.
|
|
14
|
+
*
|
|
15
|
+
* `ListState` is the same for a list a renderable appends to — a table's rows,
|
|
16
|
+
* a text's spans — which a `State<readonly T[]>` would copy on every append.
|
|
17
|
+
*
|
|
18
|
+
* `refuseRenderWrite` is that refusal, written once: a widget's observables
|
|
19
|
+
* refuse a write through it too (`src/widgets/observable.ts`).
|
|
20
|
+
*
|
|
21
|
+
* `test/seam/render-purity.test.ts` is the static half: nothing else reachable
|
|
22
|
+
* from a renderable, or from module scope, may be writable.
|
|
23
|
+
*/
|
|
24
|
+
import { type Pass } from "./protocol.js";
|
|
25
|
+
/** State was written during a render or measure that did not make it. */
|
|
26
|
+
export declare class RenderWriteError extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
export declare class State<T> {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(value: T);
|
|
32
|
+
get(): T;
|
|
33
|
+
set(value: T): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A list kept as `State` keeps a value, appended to in place: a `State` of a
|
|
37
|
+
* readonly array copies it on every append, which makes filling a 10,000-row
|
|
38
|
+
* table quadratic. `get` hands out the list itself, typed readonly, so a
|
|
39
|
+
* reader that keeps it sees later appends; `set` replaces it, leaving a kept
|
|
40
|
+
* one as it was.
|
|
41
|
+
*/
|
|
42
|
+
export declare class ListState<T> {
|
|
43
|
+
#private;
|
|
44
|
+
constructor(items?: Iterable<T>);
|
|
45
|
+
get(): readonly T[];
|
|
46
|
+
push(item: T): void;
|
|
47
|
+
set(items: Iterable<T>): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Throws unless state made in `owner` — the pass in progress when it was made,
|
|
51
|
+
* if any — may be written now: between passes, or in the pass that made it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function refuseRenderWrite(owner: Pass | undefined): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state — `State<T>`, the one place a renderable keeps state that changes after
|
|
3
|
+
* it is built, and the refusal of a write to it while something is drawn.
|
|
4
|
+
*
|
|
5
|
+
* A render changes nothing. React refuses a state update during render because
|
|
6
|
+
* state changes only through a setter, and the setter knows the render phase;
|
|
7
|
+
* this is that setter. `set` throws while a render or measure is in progress
|
|
8
|
+
* (`activePass`, owned by `render` and `measure` in `./protocol.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Ownership is React's line too: a state made during a pass belongs to that
|
|
11
|
+
* pass, and the pass may set it. A render that builds a fresh value and fills
|
|
12
|
+
* it as it draws — a `Table` it adds rows to — changes nothing anyone held
|
|
13
|
+
* before it began. Only a state that existed before the pass is refused.
|
|
14
|
+
*
|
|
15
|
+
* `ListState` is the same for a list a renderable appends to — a table's rows,
|
|
16
|
+
* a text's spans — which a `State<readonly T[]>` would copy on every append.
|
|
17
|
+
*
|
|
18
|
+
* `refuseRenderWrite` is that refusal, written once: a widget's observables
|
|
19
|
+
* refuse a write through it too (`src/widgets/observable.ts`).
|
|
20
|
+
*
|
|
21
|
+
* `test/seam/render-purity.test.ts` is the static half: nothing else reachable
|
|
22
|
+
* from a renderable, or from module scope, may be writable.
|
|
23
|
+
*/
|
|
24
|
+
import { activePass } from "./protocol.js";
|
|
25
|
+
/** State was written during a render or measure that did not make it. */
|
|
26
|
+
export class RenderWriteError extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("State was written during a render or measure. A render changes nothing: " +
|
|
29
|
+
"change state in an event handler, an update, or between frames.");
|
|
30
|
+
this.name = "RenderWriteError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class State {
|
|
34
|
+
#value;
|
|
35
|
+
#owner = activePass();
|
|
36
|
+
constructor(value) {
|
|
37
|
+
this.#value = value;
|
|
38
|
+
}
|
|
39
|
+
get() {
|
|
40
|
+
return this.#value;
|
|
41
|
+
}
|
|
42
|
+
set(value) {
|
|
43
|
+
refuseRenderWrite(this.#owner);
|
|
44
|
+
this.#value = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A list kept as `State` keeps a value, appended to in place: a `State` of a
|
|
49
|
+
* readonly array copies it on every append, which makes filling a 10,000-row
|
|
50
|
+
* table quadratic. `get` hands out the list itself, typed readonly, so a
|
|
51
|
+
* reader that keeps it sees later appends; `set` replaces it, leaving a kept
|
|
52
|
+
* one as it was.
|
|
53
|
+
*/
|
|
54
|
+
export class ListState {
|
|
55
|
+
#items;
|
|
56
|
+
#owner = activePass();
|
|
57
|
+
constructor(items = []) {
|
|
58
|
+
this.#items = [...items];
|
|
59
|
+
}
|
|
60
|
+
get() {
|
|
61
|
+
return this.#items;
|
|
62
|
+
}
|
|
63
|
+
// One item, not a rest parameter: a caller spreading a long list into one overflows the call stack.
|
|
64
|
+
push(item) {
|
|
65
|
+
refuseRenderWrite(this.#owner);
|
|
66
|
+
this.#items.push(item);
|
|
67
|
+
}
|
|
68
|
+
set(items) {
|
|
69
|
+
refuseRenderWrite(this.#owner);
|
|
70
|
+
this.#items = [...items];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Throws unless state made in `owner` — the pass in progress when it was made,
|
|
75
|
+
* if any — may be written now: between passes, or in the pass that made it.
|
|
76
|
+
*/
|
|
77
|
+
export function refuseRenderWrite(owner) {
|
|
78
|
+
const pass = activePass();
|
|
79
|
+
// [LAW:no-silent-failure] Refused loudly, as React refuses a setState in render.
|
|
80
|
+
if (pass !== undefined && pass !== owner)
|
|
81
|
+
throw new RenderWriteError();
|
|
82
|
+
}
|
package/dist/core/strip.d.ts
CHANGED
|
@@ -166,15 +166,12 @@ export type SeamCell = JoinCell | {
|
|
|
166
166
|
readonly cap: "lead" | "tail";
|
|
167
167
|
};
|
|
168
168
|
/**
|
|
169
|
-
* What a joiner draws between two neighbours, in one form:
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* indistinct from its own ground (see `SEAM_MIN_DELTA_E`), so the seam
|
|
173
|
-
* would vanish; the same width as `seam`.
|
|
169
|
+
* What a joiner draws between two neighbours, in one form: `seam`, the cells
|
|
170
|
+
* between them. Each is drawn with its ink held off its own ground
|
|
171
|
+
* (`SEAM_MIN_DELTA_E`, `standOff`).
|
|
174
172
|
*/
|
|
175
173
|
export interface SeamForm {
|
|
176
174
|
readonly seam: readonly SeamCell[];
|
|
177
|
-
readonly divider: readonly SeamCell[];
|
|
178
175
|
}
|
|
179
176
|
/**
|
|
180
177
|
* What a joiner draws at the ends of a run, in one form:
|
|
@@ -200,10 +197,10 @@ export interface JoinCaps {
|
|
|
200
197
|
readonly ascii: CapsForm;
|
|
201
198
|
}
|
|
202
199
|
/**
|
|
203
|
-
* The least ΔE_OK a
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* one cell wide.
|
|
200
|
+
* The least ΔE_OK a join cell's ink must stand off its own ground by to be
|
|
201
|
+
* seen. Below it the eye cannot tell the ink from the ground, so the joiner
|
|
202
|
+
* lifts the ink to it (`standOff`). Twice the ~.02 threshold of a visible
|
|
203
|
+
* difference, because a seam is one cell wide.
|
|
207
204
|
*/
|
|
208
205
|
export declare const SEAM_MIN_DELTA_E = 0.04;
|
|
209
206
|
/**
|
|
@@ -214,9 +211,10 @@ export declare const SEAM_MIN_DELTA_E = 0.04;
|
|
|
214
211
|
* Which part a join draws is read off its two neighbours. A neighbour is
|
|
215
212
|
* missing at the strip's ends, and counts as missing — an endpoint — when the
|
|
216
213
|
* seam paints with its ground anywhere and it has none to paint. With both
|
|
217
|
-
* present the join is the seam
|
|
218
|
-
*
|
|
219
|
-
*
|
|
214
|
+
* present the join is the seam; with only the right, the lead; with only the
|
|
215
|
+
* left, the tail; with neither, nothing. Every cell is drawn with its ink held
|
|
216
|
+
* off its ground at `options.colorSystem` (`standOff`), measured per cell, so a
|
|
217
|
+
* seam stepping in thirds shows every step however close its grounds. A cap
|
|
220
218
|
* that paints with its neighbour's ground is drawn only beside a neighbour
|
|
221
219
|
* with one, so caps that need grounds never stop a seam that needs none.
|
|
222
220
|
*
|
package/dist/core/strip.js
CHANGED
|
@@ -31,7 +31,7 @@ import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
|
|
|
31
31
|
import { Oklch } from "./oklch.js";
|
|
32
32
|
import { cellLen } from "./cells.js";
|
|
33
33
|
import { fnv1a } from "./fnv1a.js";
|
|
34
|
-
import { drawable, getStyle, isAscii } from "./protocol.js";
|
|
34
|
+
import { drawable, getStyle, isAscii, render } from "./protocol.js";
|
|
35
35
|
/**
|
|
36
36
|
* The owner every cell a strip's joiner draws is stamped with
|
|
37
37
|
* (`Segment.anchorLines`), whatever the joiner: a seam is told by what drew
|
|
@@ -80,7 +80,7 @@ export function colorsOwnedBy(anchor, owners) {
|
|
|
80
80
|
*/
|
|
81
81
|
export function drawJoin(joiner, left, right, context) {
|
|
82
82
|
const join = joiner.join(left, right, context);
|
|
83
|
-
return { render: (options) => Segment.anchorLines([[...
|
|
83
|
+
return { render: (options) => Segment.anchorLines([[...render(join, options)]], JOIN)[0] };
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* [LAW:single-enforcer] What `item` draws, every cell stamped as the item's,
|
|
@@ -89,7 +89,7 @@ export function drawJoin(joiner, left, right, context) {
|
|
|
89
89
|
* item.
|
|
90
90
|
*/
|
|
91
91
|
export function drawItem(item) {
|
|
92
|
-
return { render: (options) => Segment.anchorLines([[...
|
|
92
|
+
return { render: (options) => Segment.anchorLines([[...render(item, options)]], item)[0] };
|
|
93
93
|
}
|
|
94
94
|
// --- Strip ---
|
|
95
95
|
export class Strip {
|
|
@@ -112,12 +112,12 @@ export class Strip {
|
|
|
112
112
|
// `left` fills the canvas, and the first item would pad over the rest of
|
|
113
113
|
// the row. Where the row sits in the width is its container's to say.
|
|
114
114
|
const row = { ...options, justify: undefined };
|
|
115
|
-
yield* drawJoin(this.joiner, null, items[0], { index: 0 })
|
|
115
|
+
yield* render(drawJoin(this.joiner, null, items[0], { index: 0 }), row);
|
|
116
116
|
for (let i = 0; i < items.length; i++) {
|
|
117
117
|
const item = items[i];
|
|
118
|
-
yield* drawItem(item)
|
|
118
|
+
yield* render(drawItem(item), row);
|
|
119
119
|
const next = i + 1 < items.length ? items[i + 1] : null;
|
|
120
|
-
yield* drawJoin(this.joiner, item, next, { index: i + 1 })
|
|
120
|
+
yield* render(drawJoin(this.joiner, item, next, { index: i + 1 }), row);
|
|
121
121
|
}
|
|
122
122
|
// [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
|
|
123
123
|
// it emits, including the last, and the other block-level renderables
|
|
@@ -140,17 +140,17 @@ export class Strip {
|
|
|
140
140
|
*/
|
|
141
141
|
measure(options) {
|
|
142
142
|
let width = 0;
|
|
143
|
-
for (const segment of
|
|
143
|
+
for (const segment of render(this, options))
|
|
144
144
|
width += segment.cellLength;
|
|
145
145
|
return { minimum: width, maximum: width };
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
const PARTS = ["seam", "
|
|
148
|
+
const PARTS = ["seam", "lead", "tail"];
|
|
149
149
|
/**
|
|
150
|
-
* The least ΔE_OK a
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* one cell wide.
|
|
150
|
+
* The least ΔE_OK a join cell's ink must stand off its own ground by to be
|
|
151
|
+
* seen. Below it the eye cannot tell the ink from the ground, so the joiner
|
|
152
|
+
* lifts the ink to it (`standOff`). Twice the ~.02 threshold of a visible
|
|
153
|
+
* difference, because a seam is one cell wide.
|
|
154
154
|
*/
|
|
155
155
|
export const SEAM_MIN_DELTA_E = 0.04;
|
|
156
156
|
function* paintsIn(paint) {
|
|
@@ -170,7 +170,6 @@ const pickGlyph = (cell, part, index, at) => {
|
|
|
170
170
|
return glyphs[Math.floor((parseInt(fnv1a(`${part}:${index}:${at}`), 16) / 2 ** 32) * glyphs.length)];
|
|
171
171
|
};
|
|
172
172
|
const cellWidth = (cell) => cellLen(glyphsOf(cell)[0] ?? "");
|
|
173
|
-
const partWidth = (cells) => cells.reduce((width, cell) => width + cellWidth(cell), 0);
|
|
174
173
|
// `raw` as a `Paint`, or refused. `absent` is the neighbour this part may not
|
|
175
174
|
// name, because the part is also drawn where that neighbour does not exist.
|
|
176
175
|
function parsePaint(raw, absent, refuse) {
|
|
@@ -193,9 +192,9 @@ function parsePaint(raw, absent, refuse) {
|
|
|
193
192
|
return refuse(`has a colour that is not a Paint: ${JSON.stringify(raw)}`);
|
|
194
193
|
}
|
|
195
194
|
const parseColor = (raw, absent, refuse) => raw === "ground" ? raw : parsePaint(raw, absent, refuse);
|
|
196
|
-
const ABSENT = { seam: undefined,
|
|
195
|
+
const ABSENT = { seam: undefined, lead: "left", tail: "right" };
|
|
197
196
|
// Which argument of a joiner a part is authored in, so a refusal names it.
|
|
198
|
-
const AUTHORED_IN = { seam: "seam",
|
|
197
|
+
const AUTHORED_IN = { seam: "seam", lead: "caps", tail: "caps" };
|
|
199
198
|
function parseCell(cell, form, part, refuse) {
|
|
200
199
|
const glyph = cell.glyph;
|
|
201
200
|
const oneOf = typeof glyph === "object" && glyph !== null ? glyph.oneOf : undefined;
|
|
@@ -243,14 +242,6 @@ const asSeamCell = (form, part) => (cell, refuse) => {
|
|
|
243
242
|
refuse("names a cap and draws cells of its own; a cell is one or the other");
|
|
244
243
|
return cap === "lead" || cap === "tail" ? Object.freeze({ cap }) : refuse(`names a cap that is neither lead nor tail: ${JSON.stringify(cap)}`);
|
|
245
244
|
};
|
|
246
|
-
// A seam's width as a sum over whatever caps stand in it: its own cells', and
|
|
247
|
-
// how many times it names each cap. Two parts equal in all three are the same
|
|
248
|
-
// width under every caps.
|
|
249
|
-
const seamWidth = (cells) => [
|
|
250
|
-
partWidth(cells.filter((cell) => !("cap" in cell))),
|
|
251
|
-
cells.filter((cell) => "cap" in cell && cell.cap === "lead").length,
|
|
252
|
-
cells.filter((cell) => "cap" in cell && cell.cap === "tail").length,
|
|
253
|
-
];
|
|
254
245
|
/**
|
|
255
246
|
* [LAW:parse-dont-validate] The one place a seam is checked, so every rule
|
|
256
247
|
* the cell model states is refused at construction and the render reads cells
|
|
@@ -259,16 +250,7 @@ const seamWidth = (cells) => [
|
|
|
259
250
|
* back is a frozen copy the caller can no longer change.
|
|
260
251
|
*/
|
|
261
252
|
function parseSeam(raw) {
|
|
262
|
-
const parseForm = (form) => {
|
|
263
|
-
const seam = parseCells(raw, form, "seam", asSeamCell(form, "seam"));
|
|
264
|
-
// A divider authored as the seam itself stays the seam, so a render can see
|
|
265
|
-
// it has no choice to make.
|
|
266
|
-
const divider = raw[form].divider === raw[form].seam ? seam : parseCells(raw, form, "divider", asSeamCell(form, "divider"));
|
|
267
|
-
const [seamAt, dividerAt] = [seamWidth(seam), seamWidth(divider)];
|
|
268
|
-
if (seamAt.some((n, i) => n !== dividerAt[i]))
|
|
269
|
-
throw new Error(`seam: ${form} divider is not the width of the seam under every caps`);
|
|
270
|
-
return Object.freeze({ seam, divider });
|
|
271
|
-
};
|
|
253
|
+
const parseForm = (form) => Object.freeze({ seam: parseCells(raw, form, "seam", asSeamCell(form, "seam")) });
|
|
272
254
|
return Object.freeze({ glyph: parseForm("glyph"), ascii: parseForm("ascii") });
|
|
273
255
|
}
|
|
274
256
|
/** [LAW:parse-dont-validate] `parseSeam`'s twin for the caps. */
|
|
@@ -278,18 +260,15 @@ function parseCaps(raw) {
|
|
|
278
260
|
}
|
|
279
261
|
// One form of the seam joined with that form of the caps.
|
|
280
262
|
function joinForms(seam, caps) {
|
|
281
|
-
|
|
282
|
-
const seamCells = splice(seam.seam);
|
|
283
|
-
return { seam: seamCells, divider: seam.divider === seam.seam ? seamCells : splice(seam.divider), lead: caps.lead, tail: caps.tail };
|
|
263
|
+
return { seam: seam.seam.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell])), lead: caps.lead, tail: caps.tail };
|
|
284
264
|
}
|
|
285
265
|
// Whether any of `cells` paints with `side`'s ground.
|
|
286
266
|
const readsGround = (cells, side) => cells.some((cell) => [cell.fg, cell.bg].some((color) => color !== "ground" && [...paintsIn(color)].some((p) => "edge" in p && p.edge === side && p.part === "bg")));
|
|
287
267
|
function formOf(shape) {
|
|
288
|
-
const between = [...shape.seam, ...shape.divider];
|
|
289
268
|
return {
|
|
290
269
|
shape,
|
|
291
270
|
reads: {
|
|
292
|
-
seam: { left: readsGround(
|
|
271
|
+
seam: { left: readsGround(shape.seam, "left"), right: readsGround(shape.seam, "right") },
|
|
293
272
|
lead: readsGround(shape.lead, "right"),
|
|
294
273
|
tail: readsGround(shape.tail, "left"),
|
|
295
274
|
},
|
|
@@ -308,29 +287,82 @@ function paintableBg(bg) {
|
|
|
308
287
|
function drawnGround(bg) {
|
|
309
288
|
return bg.flattenAlpha(SURFACE_BLACK);
|
|
310
289
|
}
|
|
311
|
-
//
|
|
312
|
-
// is
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
290
|
+
// Whether ink stands off its ground, both as a depth draws them, by the floor a
|
|
291
|
+
// seam is seen at. A colour whose RGB is the terminal theme's own (ANSI 0–15, a
|
|
292
|
+
// blend reaching one) has no value here, so two of them stand apart unless they
|
|
293
|
+
// are one expression: the same palette slot — its `number`, never the name or
|
|
294
|
+
// the depth that spells it ("red", "color(1)", an EIGHT_BIT spec on 0–15 are
|
|
295
|
+
// one slot) — or the same blend, whose name spells each end by its slot for the
|
|
296
|
+
// same reason. At sixteen colours, then, the floor is a different slot. The
|
|
297
|
+
// terminal's default colour is nobody's to measure, and stands apart.
|
|
298
|
+
function standsOff(ink, ground) {
|
|
299
|
+
if (ink.isDefault || ground.isDefault)
|
|
300
|
+
return true;
|
|
301
|
+
const [a, b] = [ink.fixedValue, ground.fixedValue];
|
|
302
|
+
return a !== undefined && b !== undefined
|
|
303
|
+
? Oklch.fromRgba(a).deltaE(Oklch.fromRgba(b)) >= SEAM_MIN_DELTA_E
|
|
304
|
+
: ink.number !== undefined ? ink.number !== ground.number : ink.name !== ground.name;
|
|
305
|
+
}
|
|
306
|
+
// A step along a lightness ray: finer than the steps between the colours a
|
|
307
|
+
// truecolor writer draws, so the walk passes over none of them.
|
|
308
|
+
const LIFT_STEP = 0.001;
|
|
309
|
+
// The steps along `origin`'s lightness ray, its chroma and hue held, in the
|
|
310
|
+
// order the lift tries them: the smaller move first, and at an equal move the
|
|
311
|
+
// lighter, out to black and to white.
|
|
312
|
+
function* lightnessRay(origin) {
|
|
313
|
+
for (let k = 1; k * LIFT_STEP <= 1; k++) {
|
|
314
|
+
for (const l of [origin.l + k * LIFT_STEP, origin.l - k * LIFT_STEP]) {
|
|
315
|
+
if (l >= 0 && l <= 1)
|
|
316
|
+
yield new Oklch(l, origin.c, origin.h);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* `ink`, a glyph's colour on `ground`, held at least `SEAM_MIN_DELTA_E` off it
|
|
322
|
+
* as `depth` draws both (`Style.drawnColors`). Ink that already stands off is
|
|
323
|
+
* returned as it is. Ink that does not becomes the drawn form of the nearest
|
|
324
|
+
* step along its own OKLCH lightness ray, chroma and hue held, that stands off:
|
|
325
|
+
* the smaller step, toward white or toward black. Every step is measured as the
|
|
326
|
+
* depth draws it, so at 256 colours and at sixteen the candidates are the
|
|
327
|
+
* entries the depth draws, and the side the ink moves to can differ between
|
|
328
|
+
* depths. A floor measured on rounded colours is not monotone along a ray, so
|
|
329
|
+
* the walk enumerates and never bisects. A side with no colour to measure is
|
|
330
|
+
* left alone.
|
|
331
|
+
*
|
|
332
|
+
* [LAW:one-source-of-truth] The colour returned is the one measured — the
|
|
333
|
+
* entry the depth draws, not the step that rounds to it — so a reader of the
|
|
334
|
+
* cell at another depth (an export reads a recording in truecolor) sees the
|
|
335
|
+
* colour that cleared the floor, never a step one unit off its ground.
|
|
336
|
+
*
|
|
337
|
+
* [LAW:single-enforcer] The one place a join cell's visibility is decided:
|
|
338
|
+
* nothing outside the joiner predicts whether a seam will show.
|
|
339
|
+
*/
|
|
340
|
+
function standOff(ink, ground, depth) {
|
|
341
|
+
if (ink === undefined || ground === undefined)
|
|
342
|
+
return ink;
|
|
343
|
+
const drawn = (color, at) => new Style({ color, bgcolor: ground }).drawnColors(at);
|
|
344
|
+
const { color: shown, bgcolor: under } = drawn(ink, depth);
|
|
345
|
+
if (standsOff(shown, under))
|
|
346
|
+
return ink;
|
|
347
|
+
// The ray starts from the ink as it is laid on its ground, before a depth
|
|
348
|
+
// rounds it; ink the terminal's theme draws starts from the colour the
|
|
349
|
+
// default terminal theme gives it. Ink that vanishes is drawn in the same
|
|
350
|
+
// kind as its ground — both fixed, or both the theme's — and every step is
|
|
351
|
+
// drawn in that kind too, so it is measured as the ink was. Ink the depth
|
|
352
|
+
// draws as the theme's (a slot, or a mix reaching one at truecolor) steps
|
|
353
|
+
// between slots: an RGB beside a colour only the terminal knows could be
|
|
354
|
+
// that very colour. Ink the depth fixes (that same mix is a cube entry at
|
|
355
|
+
// 256) steps between fixed colours, for the same reason turned round.
|
|
356
|
+
const laid = drawn(ink).color;
|
|
357
|
+
const kind = shown.fixedValue === undefined ? ColorDepth.STANDARD : ColorDepth.TRUECOLOR;
|
|
358
|
+
for (const step of lightnessRay(Oklch.fromRgba(laid.getTruecolor()))) {
|
|
359
|
+
const candidate = drawn(ColorSpec.fromRgba(step.toRgba()).downgrade(kind), depth).color;
|
|
360
|
+
if (standsOff(candidate, under))
|
|
361
|
+
return candidate;
|
|
362
|
+
}
|
|
363
|
+
// [LAW:no-silent-failure] Unreachable: the ray runs out to black and to
|
|
364
|
+
// white, and no ground lies within the floor of both, nor is one slot both.
|
|
365
|
+
throw new Error(`no colour on the lightness ray of ${ink.name} stands off ${ground.name} at depth ${ColorDepth[depth]}`);
|
|
334
366
|
}
|
|
335
367
|
function resolvePaint(paint, edges, options) {
|
|
336
368
|
if ("edge" in paint) {
|
|
@@ -354,8 +386,10 @@ function cellStyle(cell, edges, options) {
|
|
|
354
386
|
// is the terminal's own background.
|
|
355
387
|
if (cell.fg === "ground")
|
|
356
388
|
return new Style({ color: cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options), reverse: true });
|
|
357
|
-
const
|
|
358
|
-
|
|
389
|
+
const bgcolor = cell.bg === "ground" ? undefined : resolvePaint(cell.bg, edges, options);
|
|
390
|
+
// [LAW:dataflow-not-control-flow] Every cell's ink is held off its ground;
|
|
391
|
+
// the gap measured between them decides how far it moves, usually not at all.
|
|
392
|
+
return new Style({ color: standOff(resolvePaint(cell.fg, edges, options), bgcolor, options.colorSystem ?? ColorDepth.TRUECOLOR), bgcolor });
|
|
359
393
|
}
|
|
360
394
|
const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
|
|
361
395
|
/**
|
|
@@ -366,9 +400,10 @@ const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyph
|
|
|
366
400
|
* Which part a join draws is read off its two neighbours. A neighbour is
|
|
367
401
|
* missing at the strip's ends, and counts as missing — an endpoint — when the
|
|
368
402
|
* seam paints with its ground anywhere and it has none to paint. With both
|
|
369
|
-
* present the join is the seam
|
|
370
|
-
*
|
|
371
|
-
*
|
|
403
|
+
* present the join is the seam; with only the right, the lead; with only the
|
|
404
|
+
* left, the tail; with neither, nothing. Every cell is drawn with its ink held
|
|
405
|
+
* off its ground at `options.colorSystem` (`standOff`), measured per cell, so a
|
|
406
|
+
* seam stepping in thirds shows every step however close its grounds. A cap
|
|
372
407
|
* that paints with its neighbour's ground is drawn only beside a neighbour
|
|
373
408
|
* with one, so caps that need grounds never stop a seam that needs none.
|
|
374
409
|
*
|
|
@@ -408,13 +443,7 @@ export class CellJoiner {
|
|
|
408
443
|
yield* styled(leads ? "lead" : "tail");
|
|
409
444
|
return;
|
|
410
445
|
}
|
|
411
|
-
|
|
412
|
-
// its ground at a shallow depth still leaves the others to show the
|
|
413
|
-
// step, and measuring per cell, not across the two grounds, divides a
|
|
414
|
-
// seam stepping in thirds where each third would not be seen.
|
|
415
|
-
const seam = styled("seam");
|
|
416
|
-
const lost = drawn.divider !== drawn.seam && seam.every((segment) => segment.style !== undefined && vanishes(segment.style, options.colorSystem));
|
|
417
|
-
yield* lost ? styled("divider") : seam;
|
|
446
|
+
yield* styled("seam");
|
|
418
447
|
},
|
|
419
448
|
};
|
|
420
449
|
}
|
|
@@ -428,52 +457,35 @@ const GROUND = "ground";
|
|
|
428
457
|
const LEAD = { cap: "lead" };
|
|
429
458
|
const TAIL = { cap: "tail" };
|
|
430
459
|
const both = (glyph, ascii) => ({ glyph, ascii });
|
|
431
|
-
const POWERLINE = { glyph: "\ue0b0",
|
|
432
|
-
const SLANT = { glyph: "\ue0bc",
|
|
433
|
-
const BACKSLANT = { glyph: "\ue0b8",
|
|
434
|
-
const FLAME = { glyph: "\ue0c0",
|
|
435
|
-
const PIXEL = { glyph: "\ue0c4",
|
|
436
|
-
const ICE = { glyph: "\ue0c8",
|
|
437
|
-
const BUBBLE = { glyph: "\ue0b4",
|
|
438
|
-
// `glyph` in the left ground over the right
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }],
|
|
442
|
-
divider: [{ glyph: thin, fg: LEFT_FG, bg: LEFT_BG }],
|
|
443
|
-
});
|
|
444
|
-
const arrowSeam = (f) => both(arrow(f.glyph, f.thin), arrow(f.ascii, f.asciiThin));
|
|
460
|
+
const POWERLINE = { glyph: "\ue0b0", ascii: ">" };
|
|
461
|
+
const SLANT = { glyph: "\ue0bc", ascii: "/" };
|
|
462
|
+
const BACKSLANT = { glyph: "\ue0b8", ascii: "\\" };
|
|
463
|
+
const FLAME = { glyph: "\ue0c0", ascii: "}" };
|
|
464
|
+
const PIXEL = { glyph: "\ue0c4", ascii: "#" };
|
|
465
|
+
const ICE = { glyph: "\ue0c8", ascii: "]" };
|
|
466
|
+
const BUBBLE = { glyph: "\ue0b4", ascii: ")" };
|
|
467
|
+
// `glyph` in the left ground over the right.
|
|
468
|
+
const arrow = (glyph) => ({ seam: [{ glyph, fg: LEFT_BG, bg: RIGHT_BG }] });
|
|
469
|
+
const arrowSeam = (f) => both(arrow(f.glyph), arrow(f.ascii));
|
|
445
470
|
// Three arrows stepping through the two thirds between the grounds.
|
|
446
|
-
function prism(glyph
|
|
471
|
+
function prism(glyph) {
|
|
447
472
|
const third = (t) => ({ mix: [LEFT_BG, RIGHT_BG, t] });
|
|
448
|
-
const pad = { glyph: " ", fg: LEFT_FG, bg: LEFT_BG };
|
|
449
473
|
return {
|
|
450
474
|
seam: [
|
|
451
475
|
{ glyph, fg: LEFT_BG, bg: third(1 / 3) },
|
|
452
476
|
{ glyph, fg: third(1 / 3), bg: third(2 / 3) },
|
|
453
477
|
{ glyph, fg: third(2 / 3), bg: RIGHT_BG },
|
|
454
478
|
],
|
|
455
|
-
divider: [pad, { glyph: thin, fg: LEFT_FG, bg: LEFT_BG }, pad],
|
|
456
479
|
};
|
|
457
480
|
}
|
|
458
481
|
// The runs parted: the caps close the one and open the other, `between` —
|
|
459
482
|
// cells on the terminal's ground — set between them.
|
|
460
|
-
|
|
461
|
-
const seam = [TAIL, ...between, LEAD];
|
|
462
|
-
return { seam, divider: seam };
|
|
463
|
-
}
|
|
483
|
+
const parted = (between) => ({ seam: [TAIL, ...between, LEAD] });
|
|
464
484
|
// One cell drawn over the two grounds' midpoint, so it reads as neither item's.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
// A ragged tear in the left ground over the midpoint; where that ink would
|
|
470
|
-
// vanish into the midpoint — grounds a few steps apart, or one ground — the
|
|
471
|
-
// same tear in the left item's text.
|
|
472
|
-
function torn(glyphs) {
|
|
473
|
-
return { seam: midpoint({ oneOf: glyphs }, LEFT_BG).seam, divider: [{ glyph: { oneOf: glyphs }, fg: LEFT_FG, bg: HALF }] };
|
|
474
|
-
}
|
|
475
|
-
const PLAIN_SEAM = [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }];
|
|
476
|
-
const PLAIN = { seam: PLAIN_SEAM, divider: PLAIN_SEAM };
|
|
485
|
+
const midpoint = (glyph, fg) => ({ seam: [{ glyph, fg, bg: HALF }] });
|
|
486
|
+
// A ragged tear in the left ground over the midpoint.
|
|
487
|
+
const torn = (glyphs) => midpoint({ oneOf: glyphs }, LEFT_BG);
|
|
488
|
+
const PLAIN = { seam: [{ glyph: " | ", fg: { role: "strip.separator" }, bg: GROUND }] };
|
|
477
489
|
const SPACE = { glyph: " ", fg: LEFT_FG, bg: GROUND };
|
|
478
490
|
const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
|
|
479
491
|
/**
|
|
@@ -483,7 +495,7 @@ const rail = (glyph) => ({ glyph, fg: { role: "strip.rail" }, bg: GROUND });
|
|
|
483
495
|
*/
|
|
484
496
|
export const SEAMS = Object.freeze({
|
|
485
497
|
powerline: parseSeam(arrowSeam(POWERLINE)),
|
|
486
|
-
prism: parseSeam(both(prism(POWERLINE.glyph
|
|
498
|
+
prism: parseSeam(both(prism(POWERLINE.glyph), prism(POWERLINE.ascii))),
|
|
487
499
|
slant: parseSeam(arrowSeam(SLANT)),
|
|
488
500
|
backslant: parseSeam(arrowSeam(BACKSLANT)),
|
|
489
501
|
flame: parseSeam(arrowSeam(FLAME)),
|
|
@@ -568,12 +580,13 @@ function gradient(steps, glyph) {
|
|
|
568
580
|
// samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
|
|
569
581
|
// so no sample equals either neighbour's ground.
|
|
570
582
|
const samples = 2 * steps;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
583
|
+
return {
|
|
584
|
+
seam: Array.from({ length: steps }, (_, i) => ({
|
|
585
|
+
glyph,
|
|
586
|
+
fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
|
|
587
|
+
bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
|
|
588
|
+
})),
|
|
589
|
+
};
|
|
577
590
|
}
|
|
578
591
|
/**
|
|
579
592
|
* A ramp of `steps` cells from the left ground to the right. Each cell is the
|