@promptctl/rich-js 0.18.1 → 0.19.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 +9 -7
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +98 -18
- package/dist/core/color.js +331 -144
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +105 -2
- package/dist/core/pretty.js +527 -97
- package/dist/core/protocol.d.ts +41 -3
- package/dist/core/protocol.js +20 -0
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/strip.d.ts +13 -2
- package/dist/core/strip.js +44 -23
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +97 -34
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +31 -19
- package/dist/host/app.js +90 -60
- package/dist/host/index.d.ts +2 -1
- package/dist/host/terminal-host.d.ts +3 -2
- package/dist/index.d.ts +17 -10
- package/dist/index.js +11 -4
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +3 -2
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +87 -30
- package/dist/renderables/embed.js +134 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +104 -16
- package/dist/renderables/progress.js +205 -81
- package/dist/renderables/progressBar.d.ts +2 -2
- package/dist/renderables/progressBar.js +30 -19
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +1 -0
- package/dist/renderables/rule.js +44 -48
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +370 -192
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.js +2 -4
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +14 -48
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/widget-app.d.ts +2 -0
- package/dist/widgets/widget-app.js +13 -6
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +11 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easing and phase: the one vocabulary for "how far along" — every function
|
|
3
|
+
* here returns progress in [0, 1].
|
|
4
|
+
*
|
|
5
|
+
* Two kinds of curve, composed as `ease(phase(t))`:
|
|
6
|
+
*
|
|
7
|
+
* - A **phase** turns a time `t` (seconds) into raw progress: `once` runs
|
|
8
|
+
* 0 → 1 over a span and holds, `loop` wraps 0 → 1 every period,
|
|
9
|
+
* `pingPong` goes 0 → 1 → 0 every period. A per-cell offset is added to `t`
|
|
10
|
+
* before the phase sees it — `phase(t + offset)` — so the cells of one
|
|
11
|
+
* effect run the same curve shifted in time.
|
|
12
|
+
* - An **ease** reshapes that progress: any `(x) => y` on [0, 1]. The
|
|
13
|
+
* built-ins are CSS's easing functions (CSS Easing Functions Level 1):
|
|
14
|
+
* `linear`, `cubic-bezier()` with its named curves, and `steps()` with its
|
|
15
|
+
* jump terms, plus `sine` for pulses — a half-cosine, so a `pingPong`
|
|
16
|
+
* through it brightens and dims with no corner at either end.
|
|
17
|
+
*
|
|
18
|
+
* [LAW:no-ambient-temporal-coupling] Nothing here reads a clock. `t` is an
|
|
19
|
+
* argument; whoever draws frames owns the clock and samples these at
|
|
20
|
+
* whatever rate it likes, so the same `t` always gives the same progress.
|
|
21
|
+
*
|
|
22
|
+
* [LAW:one-type-per-behavior] An ease is a plain function value. CSS's named
|
|
23
|
+
* curves are four instances of `cubicBezier`, not four implementations, and
|
|
24
|
+
* `ColorRamp` and every effect take the same `Ease`.
|
|
25
|
+
*
|
|
26
|
+
* Tier 0 of `src/core/`: imports nothing, so `themes/` and every later
|
|
27
|
+
* `core/` module reach it downhill. [LAW:one-way-deps]
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* CSS `cubic-bezier(x1, y1, x2, y2)`: the curve from (0, 0) to (1, 1) with
|
|
31
|
+
* those two control points, read as y at a given x. `x1` and `x2` must lie in
|
|
32
|
+
* [0, 1] — as in CSS — which keeps x monotonic in the curve parameter, so
|
|
33
|
+
* every input has exactly one output. `y1` and `y2` may leave [0, 1]; the
|
|
34
|
+
* curve then overshoots, as a CSS "back" curve does.
|
|
35
|
+
*/
|
|
36
|
+
export function cubicBezier(x1, y1, x2, y2) {
|
|
37
|
+
for (const [name, v] of [["x1", x1], ["y1", y1], ["x2", x2], ["y2", y2]]) {
|
|
38
|
+
if (!Number.isFinite(v))
|
|
39
|
+
throw new RangeError(`cubicBezier ${name} must be finite, got ${v}`);
|
|
40
|
+
}
|
|
41
|
+
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
|
|
42
|
+
throw new RangeError(`cubicBezier x1 and x2 must lie in [0, 1], got ${x1} and ${x2}`);
|
|
43
|
+
}
|
|
44
|
+
// The Bernstein form in power basis: x(s) = ((ax·s + bx)·s + cx)·s, same for y.
|
|
45
|
+
const cx = 3 * x1;
|
|
46
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
47
|
+
const ax = 1 - cx - bx;
|
|
48
|
+
const cy = 3 * y1;
|
|
49
|
+
const by = 3 * (y2 - y1) - cy;
|
|
50
|
+
const ay = 1 - cy - by;
|
|
51
|
+
const xAt = (s) => ((ax * s + bx) * s + cx) * s;
|
|
52
|
+
const yAt = (s) => ((ay * s + by) * s + cy) * s;
|
|
53
|
+
const dxAt = (s) => (3 * ax * s + 2 * bx) * s + cx;
|
|
54
|
+
const EPSILON = 1e-9;
|
|
55
|
+
// The parameter s with x(s) = x: Newton's method from s = x, which converges
|
|
56
|
+
// in a few steps on every curve CSS names; bisection when the slope is too
|
|
57
|
+
// flat for Newton to trust. x(s) is monotonic on [0, 1], so bisection always
|
|
58
|
+
// lands.
|
|
59
|
+
const solve = (x) => {
|
|
60
|
+
let s = x;
|
|
61
|
+
for (let i = 0; i < 8; i++) {
|
|
62
|
+
const err = xAt(s) - x;
|
|
63
|
+
if (Math.abs(err) < EPSILON)
|
|
64
|
+
return s;
|
|
65
|
+
const slope = dxAt(s);
|
|
66
|
+
if (Math.abs(slope) < 1e-6)
|
|
67
|
+
break;
|
|
68
|
+
s -= err / slope;
|
|
69
|
+
}
|
|
70
|
+
let lo = 0;
|
|
71
|
+
let hi = 1;
|
|
72
|
+
s = x;
|
|
73
|
+
while (hi - lo > EPSILON) {
|
|
74
|
+
if (xAt(s) < x)
|
|
75
|
+
lo = s;
|
|
76
|
+
else
|
|
77
|
+
hi = s;
|
|
78
|
+
s = (lo + hi) / 2;
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
};
|
|
82
|
+
return (x) => yAt(solve(Math.min(1, Math.max(0, x))));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* CSS `steps(n, position)`: progress held flat in `n` equal intervals, jumping
|
|
86
|
+
* between them. The output takes `n` distinct values between the jumps
|
|
87
|
+
* `position` names — CSS's step algorithm, so `steps(1, "end")` is 0 until
|
|
88
|
+
* the very end.
|
|
89
|
+
*/
|
|
90
|
+
export function steps(n, position = "jump-end") {
|
|
91
|
+
const minimum = position === "jump-none" ? 2 : 1;
|
|
92
|
+
if (!Number.isInteger(n) || n < minimum) {
|
|
93
|
+
throw new RangeError(`steps(${n}, "${position}") needs a whole number of steps ≥ ${minimum}`);
|
|
94
|
+
}
|
|
95
|
+
const jumpsAtStart = position === "jump-start" || position === "start" || position === "jump-both";
|
|
96
|
+
// How many jumps the curve makes in all, which is what each step is a fraction of.
|
|
97
|
+
const jumps = {
|
|
98
|
+
"jump-start": n,
|
|
99
|
+
start: n,
|
|
100
|
+
"jump-end": n,
|
|
101
|
+
end: n,
|
|
102
|
+
"jump-both": n + 1,
|
|
103
|
+
"jump-none": n - 1,
|
|
104
|
+
}[position];
|
|
105
|
+
const lift = jumpsAtStart ? 1 : 0;
|
|
106
|
+
return (x) => {
|
|
107
|
+
const p = Math.min(1, Math.max(0, x));
|
|
108
|
+
return Math.min(jumps, Math.floor(p * n) + lift) / jumps;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The identity ease: progress unchanged. */
|
|
112
|
+
const linear = (x) => x;
|
|
113
|
+
/** CSS `step-end`: 0 until the very end. */
|
|
114
|
+
const stepEnd = steps(1, "jump-end");
|
|
115
|
+
/** A half-cosine, flat at both ends: the curve a breathing pulse wants. */
|
|
116
|
+
const sine = (x) => (1 - Math.cos(Math.PI * x)) / 2;
|
|
117
|
+
/**
|
|
118
|
+
* Every built-in ease by the name a template spells it with — CSS's keyword
|
|
119
|
+
* names, plus `sine`, plus `step`: the spelling `ColorRamp` used before this
|
|
120
|
+
* vocabulary existed, kept so every template written against it renders the
|
|
121
|
+
* same. It is CSS's `step-end`, the same function value.
|
|
122
|
+
*/
|
|
123
|
+
export const EASES = {
|
|
124
|
+
linear,
|
|
125
|
+
ease: cubicBezier(0.25, 0.1, 0.25, 1),
|
|
126
|
+
"ease-in": cubicBezier(0.42, 0, 1, 1),
|
|
127
|
+
"ease-out": cubicBezier(0, 0, 0.58, 1),
|
|
128
|
+
"ease-in-out": cubicBezier(0.42, 0, 0.58, 1),
|
|
129
|
+
"step-start": steps(1, "jump-start"),
|
|
130
|
+
"step-end": stepEnd,
|
|
131
|
+
step: stepEnd,
|
|
132
|
+
sine,
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* The gate a spelled ease crosses. [LAW:parse-dont-validate] — a name in, the
|
|
136
|
+
* function out, so nothing downstream holds a string it must check again.
|
|
137
|
+
* Unknown names throw naming every legal one. [LAW:no-silent-failure]
|
|
138
|
+
*/
|
|
139
|
+
export function parseEase(name) {
|
|
140
|
+
if (!Object.hasOwn(EASES, name)) {
|
|
141
|
+
throw new RangeError(`unknown ease ${JSON.stringify(name)}; expected one of ` +
|
|
142
|
+
Object.keys(EASES).map((n) => JSON.stringify(n)).join(", "));
|
|
143
|
+
}
|
|
144
|
+
return EASES[name];
|
|
145
|
+
}
|
|
146
|
+
function positiveSeconds(what, seconds) {
|
|
147
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
148
|
+
throw new RangeError(`${what} must be a positive number of seconds, got ${seconds}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The three phase shapes. Each takes seconds and returns a `Phase`. */
|
|
152
|
+
export const Phase = {
|
|
153
|
+
/** 0 before `start`, rising to 1 over `duration` seconds, then held at 1. */
|
|
154
|
+
once(start, duration) {
|
|
155
|
+
if (!Number.isFinite(start))
|
|
156
|
+
throw new RangeError(`Phase.once start must be finite, got ${start}`);
|
|
157
|
+
positiveSeconds("Phase.once duration", duration);
|
|
158
|
+
return (t) => Math.min(1, Math.max(0, (t - start) / duration));
|
|
159
|
+
},
|
|
160
|
+
/** 0 → 1 every `period` seconds, wrapping back to 0; negative `t` wraps too. */
|
|
161
|
+
loop(period) {
|
|
162
|
+
positiveSeconds("Phase.loop period", period);
|
|
163
|
+
return (t) => (((t % period) + period) % period) / period;
|
|
164
|
+
},
|
|
165
|
+
/** 0 → 1 → 0 every `period` seconds: there in the first half, back in the second. */
|
|
166
|
+
pingPong(period) {
|
|
167
|
+
positiveSeconds("Phase.pingPong period", period);
|
|
168
|
+
const wrap = Phase.loop(period);
|
|
169
|
+
return (t) => 1 - Math.abs(2 * wrap(t) - 1);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env — the environment variables a program running on a terminal sees, as
|
|
3
|
+
* every public type that carries them names it.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:types-are-the-program] The strongest true theorem about what this
|
|
6
|
+
* library does with an environment is "reads a variable by name, which may be
|
|
7
|
+
* unset." It never writes one, and it never touches the rest of node's
|
|
8
|
+
* `NodeJS.ProcessEnv`. Node's `process.env` satisfies this as-is, and so does
|
|
9
|
+
* a frozen literal in a browser, where there is no `process` at all.
|
|
10
|
+
*
|
|
11
|
+
* It lives here rather than being spelled `NodeJS.ProcessEnv` because a public
|
|
12
|
+
* signature is emitted into the `.d.ts` verbatim, and a TypeScript project
|
|
13
|
+
* without `@types/node` — the ordinary browser project — cannot resolve the
|
|
14
|
+
* `NodeJS` namespace. `test/seam/browser-types.test.ts` holds that line.
|
|
15
|
+
*/
|
|
16
|
+
export type Env = Readonly<Record<string, string | undefined>>;
|
package/dist/core/env.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env — the environment variables a program running on a terminal sees, as
|
|
3
|
+
* every public type that carries them names it.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:types-are-the-program] The strongest true theorem about what this
|
|
6
|
+
* library does with an environment is "reads a variable by name, which may be
|
|
7
|
+
* unset." It never writes one, and it never touches the rest of node's
|
|
8
|
+
* `NodeJS.ProcessEnv`. Node's `process.env` satisfies this as-is, and so does
|
|
9
|
+
* a frozen literal in a browser, where there is no `process` at all.
|
|
10
|
+
*
|
|
11
|
+
* It lives here rather than being spelled `NodeJS.ProcessEnv` because a public
|
|
12
|
+
* signature is emitted into the `.d.ts` verbatim, and a TypeScript project
|
|
13
|
+
* without `@types/node` — the ordinary browser project — cannot resolve the
|
|
14
|
+
* `NodeJS` namespace. `test/seam/browser-types.test.ts` holds that line.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/dist/core/export-html.js
CHANGED
|
@@ -13,10 +13,7 @@
|
|
|
13
13
|
* is narrower than the terminal was.
|
|
14
14
|
*/
|
|
15
15
|
import { cellLen, graphemes } from "./cells.js";
|
|
16
|
-
import { exportCanvas, exportLines } from "./export-lines.js";
|
|
17
|
-
const ENTITIES = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
18
|
-
const escapeText = (text) => text.replace(/[&<>]/g, (c) => ENTITIES[c]);
|
|
19
|
-
const escapeAttribute = (value) => value.replace(/[&"<>]/g, (c) => ENTITIES[c]);
|
|
16
|
+
import { escapeAttribute, escapeText, exportCanvas, exportLines, } from "./export-lines.js";
|
|
20
17
|
const BLINK_KEYFRAMES = "rich-blink";
|
|
21
18
|
// [LAW:dataflow-not-control-flow] Each enum-valued field of a look is a table
|
|
22
19
|
// from its values to the declarations it adds, so every value is spelled once
|
|
@@ -77,6 +77,14 @@ export type ExportLine = readonly ExportRun[];
|
|
|
77
77
|
* it.
|
|
78
78
|
*/
|
|
79
79
|
export declare function parseHref(link: string): Href | null;
|
|
80
|
+
/**
|
|
81
|
+
* Text as character data in HTML or SVG markup. Both exporters write markup,
|
|
82
|
+
* and [LAW:single-enforcer] one escaper is what keeps a title or a run that is
|
|
83
|
+
* safe in one format from being live markup, or no markup at all, in the other.
|
|
84
|
+
*/
|
|
85
|
+
export declare const escapeText: (text: string) => string;
|
|
86
|
+
/** A value inside a double-quoted attribute in HTML or SVG markup. */
|
|
87
|
+
export declare const escapeAttribute: (value: string) => string;
|
|
80
88
|
/** What an unstyled cell shows under a theme: the canvas, and ink on it. */
|
|
81
89
|
export interface ExportCanvas {
|
|
82
90
|
readonly background: ColorRgba;
|
|
@@ -85,25 +93,26 @@ export interface ExportCanvas {
|
|
|
85
93
|
/**
|
|
86
94
|
* The ground every export is drawn on under `theme`.
|
|
87
95
|
*
|
|
88
|
-
* An exporter paints its page or window with this
|
|
89
|
-
*
|
|
90
|
-
* page around it cannot come out different colours.
|
|
96
|
+
* An exporter paints its page or window with this, and a run with no
|
|
97
|
+
* background of its own resolves to `"canvas"` rather than a colour, so a run
|
|
98
|
+
* showing the canvas and the page around it cannot come out different colours.
|
|
91
99
|
*/
|
|
92
100
|
export declare function exportCanvas(theme?: TerminalTheme): ExportCanvas;
|
|
93
101
|
/**
|
|
94
102
|
* A `Style` as it appears on screen under `theme`.
|
|
95
103
|
*
|
|
96
|
-
* The order is the terminal's: colours
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* present, and still selectable, in both formats.
|
|
104
|
+
* The order is the terminal's: the colours are `Style.drawnColors`, the ones
|
|
105
|
+
* `toSgrCodes` writes, resolved through the theme — `reverse` swaps them,
|
|
106
|
+
* `dim` fades the glyph toward whatever background it ended up on, and
|
|
107
|
+
* `conceal` finally paints the glyph in that background. Every step works on
|
|
108
|
+
* opaque colour, so no exporter ever draws a translucent one. Text under
|
|
109
|
+
* `conceal` is still present, and still selectable, in both formats.
|
|
103
110
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
111
|
+
* A translucent colour is flattened where the writer flattens it, over
|
|
112
|
+
* `SURFACE_BLACK`, not over this export's canvas. The writer's truecolor
|
|
113
|
+
* bytes are absolute — no terminal theme retints them — so a terminal under
|
|
114
|
+
* `theme` shows that colour, and an export that flattened over its own canvas
|
|
115
|
+
* would picture a colour no terminal draws.
|
|
107
116
|
*
|
|
108
117
|
* `theme` omitted is `ColorSpec.getTruecolor`'s own fallback — black canvas,
|
|
109
118
|
* white ink, the standard ANSI table. Choosing it there rather than naming a
|
|
@@ -55,6 +55,22 @@ export function parseHref(link) {
|
|
|
55
55
|
const url = new URL(link);
|
|
56
56
|
return LINKABLE_SCHEMES.has(url.protocol) ? url.href : null;
|
|
57
57
|
}
|
|
58
|
+
const ENTITIES = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
59
|
+
/**
|
|
60
|
+
* The characters XML 1.0 admits nowhere in a document: C0 controls but tab,
|
|
61
|
+
* newline and return, the two noncharacters U+FFFE and U+FFFF, and unpaired
|
|
62
|
+
* surrogates. One of them leaves a whole SVG unparsable, not one glyph missing.
|
|
63
|
+
* Each takes no cell, so dropping it moves nothing on the grid.
|
|
64
|
+
*/
|
|
65
|
+
const NOT_XML = /[\x00-\x08\x0B\x0C\x0E-\x1F]|\p{Cs}/gu;
|
|
66
|
+
/**
|
|
67
|
+
* Text as character data in HTML or SVG markup. Both exporters write markup,
|
|
68
|
+
* and [LAW:single-enforcer] one escaper is what keeps a title or a run that is
|
|
69
|
+
* safe in one format from being live markup, or no markup at all, in the other.
|
|
70
|
+
*/
|
|
71
|
+
export const escapeText = (text) => text.replace(NOT_XML, "").replace(/[&<>]/g, (c) => ENTITIES[c]);
|
|
72
|
+
/** A value inside a double-quoted attribute in HTML or SVG markup. */
|
|
73
|
+
export const escapeAttribute = (value) => value.replace(NOT_XML, "").replace(/[&"<>]/g, (c) => ENTITIES[c]);
|
|
58
74
|
/**
|
|
59
75
|
* How far a dim glyph moves toward its background — Rich's own factor, so an
|
|
60
76
|
* export of the same program matches the library this ports.
|
|
@@ -64,9 +80,9 @@ const DEFAULT_COLOR = ColorSpec.default();
|
|
|
64
80
|
/**
|
|
65
81
|
* The ground every export is drawn on under `theme`.
|
|
66
82
|
*
|
|
67
|
-
* An exporter paints its page or window with this
|
|
68
|
-
*
|
|
69
|
-
* page around it cannot come out different colours.
|
|
83
|
+
* An exporter paints its page or window with this, and a run with no
|
|
84
|
+
* background of its own resolves to `"canvas"` rather than a colour, so a run
|
|
85
|
+
* showing the canvas and the page around it cannot come out different colours.
|
|
70
86
|
*/
|
|
71
87
|
export function exportCanvas(theme) {
|
|
72
88
|
return {
|
|
@@ -77,29 +93,31 @@ export function exportCanvas(theme) {
|
|
|
77
93
|
/**
|
|
78
94
|
* A `Style` as it appears on screen under `theme`.
|
|
79
95
|
*
|
|
80
|
-
* The order is the terminal's: colours
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* present, and still selectable, in both formats.
|
|
96
|
+
* The order is the terminal's: the colours are `Style.drawnColors`, the ones
|
|
97
|
+
* `toSgrCodes` writes, resolved through the theme — `reverse` swaps them,
|
|
98
|
+
* `dim` fades the glyph toward whatever background it ended up on, and
|
|
99
|
+
* `conceal` finally paints the glyph in that background. Every step works on
|
|
100
|
+
* opaque colour, so no exporter ever draws a translucent one. Text under
|
|
101
|
+
* `conceal` is still present, and still selectable, in both formats.
|
|
87
102
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
103
|
+
* A translucent colour is flattened where the writer flattens it, over
|
|
104
|
+
* `SURFACE_BLACK`, not over this export's canvas. The writer's truecolor
|
|
105
|
+
* bytes are absolute — no terminal theme retints them — so a terminal under
|
|
106
|
+
* `theme` shows that colour, and an export that flattened over its own canvas
|
|
107
|
+
* would picture a colour no terminal draws.
|
|
91
108
|
*
|
|
92
109
|
* `theme` omitted is `ColorSpec.getTruecolor`'s own fallback — black canvas,
|
|
93
110
|
* white ink, the standard ANSI table. Choosing it there rather than naming a
|
|
94
111
|
* preset here is what keeps `core/` from a third upward edge into `themes/`.
|
|
95
112
|
*/
|
|
96
113
|
export function resolveLook(style, theme) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
114
|
+
// [LAW:one-source-of-truth] `drawnColors` is the one account of what a style
|
|
115
|
+
// puts on screen; the export reads it rather than flattening a second way.
|
|
116
|
+
const drawn = style.drawnColors();
|
|
117
|
+
const inkSpec = drawn.color ?? DEFAULT_COLOR;
|
|
118
|
+
const paperSpec = drawn.bgcolor ?? DEFAULT_COLOR;
|
|
119
|
+
const paper = paperSpec.getTruecolor(theme, false);
|
|
120
|
+
const ink = inkSpec.getTruecolor(theme, true);
|
|
103
121
|
// A reversed run always paints: its background is the ink, which is never
|
|
104
122
|
// the canvas, even when the ink is the theme's default foreground.
|
|
105
123
|
const glyph = style.reverse ? paper : ink;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-svg — recorded segments drawn as an SVG terminal window: rounded
|
|
3
|
+
* chrome with a title and three window buttons, the rows inside it, every
|
|
4
|
+
* glyph where its terminal cell was, and the text still selectable.
|
|
5
|
+
*
|
|
6
|
+
* An encoding of `export-lines` and nothing more, the same as `export-html`.
|
|
7
|
+
* What a run looks like under a theme is decided there; this module decides
|
|
8
|
+
* only where SVG puts it. It never sees a `Style`.
|
|
9
|
+
*
|
|
10
|
+
* The geometry is Rich's — a 20px character, a cell 0.61 of that wide, a row
|
|
11
|
+
* 1.22 of it tall, the same margin and padding — so a screenshot of the same
|
|
12
|
+
* program matches the library this ports. The frame's colour is not Rich's:
|
|
13
|
+
* Rich strokes it white at 35% whatever the theme, which vanishes around a
|
|
14
|
+
* light canvas. Here it is the theme's ink blended 35% into the canvas — over
|
|
15
|
+
* a black canvas the colour Rich draws — and opaque, so the half of the stroke
|
|
16
|
+
* lying outside the canvas does not take on the colour of the page around it.
|
|
17
|
+
*
|
|
18
|
+
* Nor is how a glyph lands on its cell. The exporter has no font engine and
|
|
19
|
+
* does not know which font the viewer will draw with, so no advance width can
|
|
20
|
+
* be trusted: a CJK character drawn from a fallback font is rarely two cells,
|
|
21
|
+
* and every glyph after it on the row drifts. So every chunk of a run is
|
|
22
|
+
* anchored at its own column and stretched to its own cells with
|
|
23
|
+
* `textLength`, and alignment is guaranteed by the numbers written here rather
|
|
24
|
+
* than by the font. Measured in headless Chromium on `"ab漢字cd😀ef é̂x"`:
|
|
25
|
+
* Rich's `textLength` (cell width × string length) was off by up to 12px, one
|
|
26
|
+
* `<text>` a run by 3.9px, this chunking by 0.
|
|
27
|
+
*
|
|
28
|
+
* Decorations are rectangles, not CSS `text-decoration`: SVG has no reliable
|
|
29
|
+
* double underline, and one `text-decoration` property cannot hold an
|
|
30
|
+
* underline and a strike with different styles.
|
|
31
|
+
*/
|
|
32
|
+
import { type TerminalTheme } from "./color.js";
|
|
33
|
+
import type { Segment } from "./segment.js";
|
|
34
|
+
export interface SvgOptions {
|
|
35
|
+
readonly theme?: TerminalTheme;
|
|
36
|
+
/** The window title, centred in the chrome. */
|
|
37
|
+
readonly title: string;
|
|
38
|
+
/** The console width in cells; a wider row widens the window rather than being clipped. */
|
|
39
|
+
readonly width: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `segments` under `theme` as a standalone SVG document.
|
|
43
|
+
*
|
|
44
|
+
* Every background is painted before any glyph, so no run's paint covers its
|
|
45
|
+
* neighbour's text. Whitespace is emitted as real spaces under
|
|
46
|
+
* `xml:space="preserve"`, never as ` `, so a selection copies what the
|
|
47
|
+
* terminal showed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function encodeSvg(segments: Iterable<Segment>, { theme, title, width }: SvgOptions): string;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-svg — recorded segments drawn as an SVG terminal window: rounded
|
|
3
|
+
* chrome with a title and three window buttons, the rows inside it, every
|
|
4
|
+
* glyph where its terminal cell was, and the text still selectable.
|
|
5
|
+
*
|
|
6
|
+
* An encoding of `export-lines` and nothing more, the same as `export-html`.
|
|
7
|
+
* What a run looks like under a theme is decided there; this module decides
|
|
8
|
+
* only where SVG puts it. It never sees a `Style`.
|
|
9
|
+
*
|
|
10
|
+
* The geometry is Rich's — a 20px character, a cell 0.61 of that wide, a row
|
|
11
|
+
* 1.22 of it tall, the same margin and padding — so a screenshot of the same
|
|
12
|
+
* program matches the library this ports. The frame's colour is not Rich's:
|
|
13
|
+
* Rich strokes it white at 35% whatever the theme, which vanishes around a
|
|
14
|
+
* light canvas. Here it is the theme's ink blended 35% into the canvas — over
|
|
15
|
+
* a black canvas the colour Rich draws — and opaque, so the half of the stroke
|
|
16
|
+
* lying outside the canvas does not take on the colour of the page around it.
|
|
17
|
+
*
|
|
18
|
+
* Nor is how a glyph lands on its cell. The exporter has no font engine and
|
|
19
|
+
* does not know which font the viewer will draw with, so no advance width can
|
|
20
|
+
* be trusted: a CJK character drawn from a fallback font is rarely two cells,
|
|
21
|
+
* and every glyph after it on the row drifts. So every chunk of a run is
|
|
22
|
+
* anchored at its own column and stretched to its own cells with
|
|
23
|
+
* `textLength`, and alignment is guaranteed by the numbers written here rather
|
|
24
|
+
* than by the font. Measured in headless Chromium on `"ab漢字cd😀ef é̂x"`:
|
|
25
|
+
* Rich's `textLength` (cell width × string length) was off by up to 12px, one
|
|
26
|
+
* `<text>` a run by 3.9px, this chunking by 0.
|
|
27
|
+
*
|
|
28
|
+
* Decorations are rectangles, not CSS `text-decoration`: SVG has no reliable
|
|
29
|
+
* double underline, and one `text-decoration` property cannot hold an
|
|
30
|
+
* underline and a strike with different styles.
|
|
31
|
+
*/
|
|
32
|
+
import { cellLen, graphemes } from "./cells.js";
|
|
33
|
+
import { blendRgb } from "./color.js";
|
|
34
|
+
import { fnv1a } from "./fnv1a.js";
|
|
35
|
+
import { escapeAttribute, escapeText, exportCanvas, exportLines, } from "./export-lines.js";
|
|
36
|
+
const CHAR_HEIGHT = 20;
|
|
37
|
+
const CELL_WIDTH = CHAR_HEIGHT * 0.61;
|
|
38
|
+
const LINE_HEIGHT = CHAR_HEIGHT * 1.22;
|
|
39
|
+
const MARGIN = 1;
|
|
40
|
+
const PADDING = { top: 40, right: 8, bottom: 8, left: 8 };
|
|
41
|
+
/** A painted background sits 1.5px into its row and overlaps the next by a hair, so rows show no seam. */
|
|
42
|
+
const CELL_TOP = 1.5;
|
|
43
|
+
const CELL_HEIGHT = LINE_HEIGHT + 0.25;
|
|
44
|
+
const LINE_THICKNESS = 1.5;
|
|
45
|
+
const FONT_FAMILY = `"Fira Code", Menlo, Consolas, "DejaVu Sans Mono", monospace`;
|
|
46
|
+
const FRAME_FADE = 0.35;
|
|
47
|
+
const WINDOW_BUTTONS = ["#ff5f57", "#febc2e", "#28c840"];
|
|
48
|
+
/** Coordinates to two decimals, so `3 × 12.2` is written `36.6` and not `36.599999999999994`. */
|
|
49
|
+
const n = (value) => String(Math.round(value * 100) / 100);
|
|
50
|
+
/**
|
|
51
|
+
* The blink keyframes every SVG defines identically, so one name serves them
|
|
52
|
+
* all: two inlined in one page define the same rule twice, and neither changes
|
|
53
|
+
* the other.
|
|
54
|
+
*/
|
|
55
|
+
const BLINK_KEYFRAMES = "rich-svg-blink";
|
|
56
|
+
/**
|
|
57
|
+
* `text` cut into the pieces that are positioned one by one, starting at
|
|
58
|
+
* `column`: consecutive one-cell graphemes share a chunk, since the font's
|
|
59
|
+
* own advance is right for them to within what `textLength` corrects; every
|
|
60
|
+
* wider grapheme is a chunk of its own, since that advance is the one that
|
|
61
|
+
* drifts. A zero-cell grapheme joins the chunk before it so it can still
|
|
62
|
+
* combine, and only when a run opens with one does it stand as a chunk of no
|
|
63
|
+
* cells. Graphemes rather than code points, because `cellLen` measures a
|
|
64
|
+
* joined emoji as one glyph.
|
|
65
|
+
*/
|
|
66
|
+
function chunk(text, column) {
|
|
67
|
+
const pieces = [];
|
|
68
|
+
for (const grapheme of graphemes(text)) {
|
|
69
|
+
const cells = cellLen(grapheme);
|
|
70
|
+
const last = pieces[pieces.length - 1];
|
|
71
|
+
if (last !== undefined && (cells === 0 || (cells === 1 && last.narrow))) {
|
|
72
|
+
last.text += grapheme;
|
|
73
|
+
last.cells += cells;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
pieces.push({ text: grapheme, cells, narrow: cells === 1 });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let at = column;
|
|
80
|
+
return pieces.map(({ text, cells }) => {
|
|
81
|
+
const placed = { text, column: at, cells };
|
|
82
|
+
at += cells;
|
|
83
|
+
return placed;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Every run of every row at the column the runs before it on its row end at. */
|
|
87
|
+
function place(rows) {
|
|
88
|
+
return rows.flatMap((runs, row) => {
|
|
89
|
+
let column = 0;
|
|
90
|
+
return runs.map(({ text, look }) => {
|
|
91
|
+
const chunks = chunk(text, column);
|
|
92
|
+
const cells = chunks.reduce((sum, c) => sum + c.cells, 0);
|
|
93
|
+
const placed = { row, column, cells, look, chunks };
|
|
94
|
+
column += cells;
|
|
95
|
+
return placed;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// [LAW:dataflow-not-control-flow] Each enum-valued field of a look is a table
|
|
100
|
+
// from its values to what it draws, so `Record` makes a new value a compile
|
|
101
|
+
// error until it has a row.
|
|
102
|
+
const UNDERLINE_BELOW_BASELINE = {
|
|
103
|
+
none: [],
|
|
104
|
+
single: [2],
|
|
105
|
+
double: [2, 5],
|
|
106
|
+
};
|
|
107
|
+
const BLINK_PERIOD = {
|
|
108
|
+
none: null,
|
|
109
|
+
slow: "1s",
|
|
110
|
+
fast: "0.5s",
|
|
111
|
+
};
|
|
112
|
+
const OUTLINE_RADIUS = {
|
|
113
|
+
none: null,
|
|
114
|
+
frame: 0,
|
|
115
|
+
encircle: CELL_WIDTH,
|
|
116
|
+
};
|
|
117
|
+
/** The CSS a look's class carries: what a glyph's `fill` and font say, and its blink. */
|
|
118
|
+
function lookCss(look) {
|
|
119
|
+
const period = BLINK_PERIOD[look.blink];
|
|
120
|
+
return [
|
|
121
|
+
`fill:${look.foreground.hex}`,
|
|
122
|
+
...(look.bold ? ["font-weight:bold"] : []),
|
|
123
|
+
...(look.italic ? ["font-style:italic"] : []),
|
|
124
|
+
...(period === null ? [] : [`animation:${BLINK_KEYFRAMES} ${period} step-end infinite`]),
|
|
125
|
+
].join(";");
|
|
126
|
+
}
|
|
127
|
+
const cellBox = (run) => `x="${n(run.column * CELL_WIDTH)}" y="${n(run.row * LINE_HEIGHT + CELL_TOP)}" ` +
|
|
128
|
+
`width="${n(run.cells * CELL_WIDTH)}" height="${n(CELL_HEIGHT)}"`;
|
|
129
|
+
/** A run's painted background; a run showing the canvas paints nothing. */
|
|
130
|
+
function backgroundSvg(run) {
|
|
131
|
+
return run.look.background === "canvas"
|
|
132
|
+
? []
|
|
133
|
+
: [`<rect fill="${run.look.background.hex}" ${cellBox(run)} shape-rendering="crispEdges"/>`];
|
|
134
|
+
}
|
|
135
|
+
function textSvg({ text, column, cells }, baseline) {
|
|
136
|
+
// A chunk of no cells has no length to stretch to; it takes the font's.
|
|
137
|
+
const length = cells === 0 ? "" : ` textLength="${n(cells * CELL_WIDTH)}" lengthAdjust="spacingAndGlyphs"`;
|
|
138
|
+
return `<text x="${n(column * CELL_WIDTH)}" y="${n(baseline)}"${length} xml:space="preserve">${escapeText(text)}</text>`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One run: its glyphs, then its lines and outline over them, in a group whose
|
|
142
|
+
* class carries the look — so the lines take the glyph's `fill` and blink
|
|
143
|
+
* with it. The outline is a stroke, so it names its colour itself.
|
|
144
|
+
*/
|
|
145
|
+
function runSvg(run, className) {
|
|
146
|
+
const { look } = run;
|
|
147
|
+
const top = run.row * LINE_HEIGHT;
|
|
148
|
+
const baseline = top + CHAR_HEIGHT;
|
|
149
|
+
const x = n(run.column * CELL_WIDTH);
|
|
150
|
+
const width = n(run.cells * CELL_WIDTH);
|
|
151
|
+
const lineYs = [
|
|
152
|
+
...UNDERLINE_BELOW_BASELINE[look.underline].map((offset) => baseline + offset),
|
|
153
|
+
...(look.strike ? [baseline - 6] : []),
|
|
154
|
+
...(look.overline ? [top + CELL_TOP] : []),
|
|
155
|
+
];
|
|
156
|
+
const radius = OUTLINE_RADIUS[look.outline];
|
|
157
|
+
const drawn = [
|
|
158
|
+
...run.chunks.map((c) => textSvg(c, baseline)),
|
|
159
|
+
...lineYs.map((y) => `<rect x="${x}" y="${n(y)}" width="${width}" height="${LINE_THICKNESS}"/>`),
|
|
160
|
+
...(radius === null
|
|
161
|
+
? []
|
|
162
|
+
: [`<rect ${cellBox(run)} rx="${n(radius)}" fill="none" stroke="${look.foreground.hex}" stroke-width="1"/>`]),
|
|
163
|
+
].join("");
|
|
164
|
+
const group = `<g class="${className}">${drawn}</g>`;
|
|
165
|
+
return look.href === null ? group : `<a href="${escapeAttribute(look.href)}">${group}</a>`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* `segments` under `theme` as a standalone SVG document.
|
|
169
|
+
*
|
|
170
|
+
* Every background is painted before any glyph, so no run's paint covers its
|
|
171
|
+
* neighbour's text. Whitespace is emitted as real spaces under
|
|
172
|
+
* `xml:space="preserve"`, never as ` `, so a selection copies what the
|
|
173
|
+
* terminal showed.
|
|
174
|
+
*/
|
|
175
|
+
export function encodeSvg(segments, { theme, title, width }) {
|
|
176
|
+
const canvas = exportCanvas(theme);
|
|
177
|
+
const rows = exportLines(segments, theme);
|
|
178
|
+
const runs = place(rows);
|
|
179
|
+
// The prefix every class name carries is a hash of the rules those classes
|
|
180
|
+
// define as well as of what is drawn, since a class is page-wide once the
|
|
181
|
+
// SVG is inlined: two exports that share a prefix define every rule alike, so
|
|
182
|
+
// the same recording under another theme cannot restyle this one.
|
|
183
|
+
const styled = runs.map((run) => ({ run, css: lookCss(run.look) }));
|
|
184
|
+
const rules = [...new Set(styled.map(({ css }) => css))];
|
|
185
|
+
const text = rows.map((row) => row.map((run) => run.text).join("")).join("\n");
|
|
186
|
+
const id = `terminal-${fnv1a(`${title.length}:${title}${text.length}:${text}${rules.join("\n")}`)}`;
|
|
187
|
+
const classOf = (css) => `${id}-r${rules.indexOf(css) + 1}`;
|
|
188
|
+
const drawnRuns = styled.map(({ run, css }) => runSvg(run, classOf(css)));
|
|
189
|
+
const columns = runs.reduce((widest, run) => Math.max(widest, run.column + run.cells), width);
|
|
190
|
+
const terminalWidth = Math.ceil(columns * CELL_WIDTH + PADDING.left + PADDING.right);
|
|
191
|
+
const terminalHeight = rows.length * LINE_HEIGHT + PADDING.top + PADDING.bottom;
|
|
192
|
+
const stylesheet = [
|
|
193
|
+
`.${id}-matrix{font-family:${FONT_FAMILY};font-size:${CHAR_HEIGHT}px}`,
|
|
194
|
+
`.${id}-title{font-size:18px;font-weight:bold;font-family:arial}`,
|
|
195
|
+
...rules.map((css) => `.${classOf(css)}{${css}}`),
|
|
196
|
+
// Blink only for a reader who has not asked for reduced motion, as in HTML.
|
|
197
|
+
`@media (prefers-reduced-motion:no-preference){@keyframes ${BLINK_KEYFRAMES}{50%{fill:transparent}}}`,
|
|
198
|
+
].join("\n");
|
|
199
|
+
return [
|
|
200
|
+
`<svg class="rich-terminal" viewBox="0 0 ${n(terminalWidth + 2 * MARGIN)} ${n(terminalHeight + 2 * MARGIN)}" xmlns="http://www.w3.org/2000/svg">`,
|
|
201
|
+
`<style>\n${stylesheet}\n</style>`,
|
|
202
|
+
`<rect fill="${canvas.background.hex}" stroke="${blendRgb(canvas.background, canvas.foreground, FRAME_FADE).hex}" stroke-width="1" ` +
|
|
203
|
+
`x="${MARGIN}" y="${MARGIN}" width="${n(terminalWidth)}" height="${n(terminalHeight)}" rx="8"/>`,
|
|
204
|
+
`<text class="${id}-title" fill="${canvas.foreground.hex}" text-anchor="middle" ` +
|
|
205
|
+
`x="${n(MARGIN + terminalWidth / 2)}" y="${MARGIN + CHAR_HEIGHT + 6}">${escapeText(title)}</text>`,
|
|
206
|
+
`<g transform="translate(26,22)">${WINDOW_BUTTONS.map((fill, i) => `<circle cx="${i * 22}" cy="0" r="7" fill="${fill}"/>`).join("")}</g>`,
|
|
207
|
+
`<g class="${id}-matrix" transform="translate(${MARGIN + PADDING.left},${MARGIN + PADDING.top})">`,
|
|
208
|
+
...runs.flatMap(backgroundSvg),
|
|
209
|
+
...drawnRuns,
|
|
210
|
+
`</g>`,
|
|
211
|
+
`</svg>`,
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fnv1a — FNV-1a, 32-bit, over a string's UTF-8 bytes, as 8 lowercase hex
|
|
3
|
+
* digits: the short deterministic name this library derives wherever output
|
|
4
|
+
* needs an identifier that is a function of what it names — an OSC 8 link id,
|
|
5
|
+
* an SVG's class prefix — rather than of a counter or a clock.
|
|
6
|
+
*
|
|
7
|
+
* UTF-8 rather than UTF-16 code units, so any other runtime computing standard
|
|
8
|
+
* FNV-1a over the same text gets the same digits.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fnv1a(text: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fnv1a — FNV-1a, 32-bit, over a string's UTF-8 bytes, as 8 lowercase hex
|
|
3
|
+
* digits: the short deterministic name this library derives wherever output
|
|
4
|
+
* needs an identifier that is a function of what it names — an OSC 8 link id,
|
|
5
|
+
* an SVG's class prefix — rather than of a counter or a clock.
|
|
6
|
+
*
|
|
7
|
+
* UTF-8 rather than UTF-16 code units, so any other runtime computing standard
|
|
8
|
+
* FNV-1a over the same text gets the same digits.
|
|
9
|
+
*/
|
|
10
|
+
const utf8 = new TextEncoder();
|
|
11
|
+
export function fnv1a(text) {
|
|
12
|
+
let hash = 0x811c9dc5;
|
|
13
|
+
for (const byte of utf8.encode(text)) {
|
|
14
|
+
hash = Math.imul(hash ^ byte, 0x01000193);
|
|
15
|
+
}
|
|
16
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
17
|
+
}
|