@promptctl/rich-js 0.18.0 → 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/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +145 -0
- package/dist/core/box.js +372 -0
- package/dist/core/cells.d.ts +192 -0
- package/dist/core/cells.js +334 -0
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +326 -0
- package/dist/core/color.js +1225 -0
- package/dist/core/console.d.ts +196 -0
- package/dist/core/console.js +596 -0
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +197 -0
- package/dist/core/export-lines.d.ts +130 -0
- package/dist/core/export-lines.js +159 -0
- 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 +33 -0
- package/dist/core/highlighter.js +190 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +140 -0
- package/dist/core/markup.js +654 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +67 -0
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +78 -0
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +49 -0
- package/dist/core/pretty.d.ts +247 -0
- package/dist/core/pretty.js +906 -0
- package/dist/core/protocol.d.ts +305 -0
- package/dist/core/protocol.js +183 -0
- package/dist/core/render.d.ts +87 -0
- package/dist/core/render.js +105 -0
- package/dist/core/segment.d.ts +162 -0
- package/dist/core/segment.js +488 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +152 -0
- package/dist/core/strip.js +326 -0
- package/dist/core/style.d.ts +189 -0
- package/dist/core/style.js +758 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +283 -0
- package/dist/core/text.js +1097 -0
- package/dist/core/wrap.d.ts +46 -0
- package/dist/core/wrap.js +140 -0
- package/dist/host/app.d.ts +133 -0
- package/dist/host/app.js +285 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +180 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +114 -0
- package/dist/node/prompt.d.ts +34 -0
- package/dist/node/prompt.js +51 -0
- package/dist/node/save.d.ts +18 -0
- package/dist/node/save.js +24 -0
- package/dist/node/terminal-host.d.ts +114 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +38 -0
- package/dist/node/traceback.js +98 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +26 -0
- package/dist/renderables/columns.d.ts +77 -0
- package/dist/renderables/columns.js +191 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +31 -0
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +107 -0
- package/dist/renderables/embed.js +160 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +14 -0
- package/dist/renderables/group.js +22 -0
- package/dist/renderables/layout.d.ts +75 -0
- package/dist/renderables/layout.js +223 -0
- package/dist/renderables/live.d.ts +74 -0
- package/dist/renderables/live.js +188 -0
- package/dist/renderables/markdown.d.ts +38 -0
- package/dist/renderables/markdown.js +587 -0
- package/dist/renderables/padding.d.ts +59 -0
- package/dist/renderables/padding.js +147 -0
- package/dist/renderables/panel.d.ts +114 -0
- package/dist/renderables/panel.js +303 -0
- package/dist/renderables/progress.d.ts +178 -0
- package/dist/renderables/progress.js +362 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +64 -0
- package/dist/renderables/prompt.d.ts +71 -0
- package/dist/renderables/prompt.js +165 -0
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +26 -0
- package/dist/renderables/rule.js +86 -0
- package/dist/renderables/spinner.d.ts +57 -0
- package/dist/renderables/spinner.js +103 -0
- package/dist/renderables/status.d.ts +29 -0
- package/dist/renderables/status.js +46 -0
- package/dist/renderables/syntax.d.ts +65 -0
- package/dist/renderables/syntax.js +166 -0
- package/dist/renderables/table.d.ts +232 -0
- package/dist/renderables/table.js +973 -0
- package/dist/renderables/traceback.d.ts +32 -0
- package/dist/renderables/traceback.js +279 -0
- package/dist/renderables/tree.d.ts +76 -0
- package/dist/renderables/tree.js +177 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +196 -0
- package/dist/template-bindings/color-funcs.d.ts +109 -0
- package/dist/template-bindings/color-funcs.js +243 -0
- package/dist/template-bindings/helpers.d.ts +26 -0
- package/dist/template-bindings/helpers.js +33 -0
- package/dist/template-bindings/index.d.ts +119 -0
- package/dist/template-bindings/index.js +162 -0
- package/dist/template-bindings/palette-funcs.d.ts +96 -0
- package/dist/template-bindings/palette-funcs.js +145 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +157 -0
- package/dist/themes/buildPalette.d.ts +57 -0
- package/dist/themes/buildPalette.js +100 -0
- package/dist/themes/colorMath.d.ts +94 -0
- package/dist/themes/colorMath.js +280 -0
- package/dist/themes/colorRef.d.ts +36 -0
- package/dist/themes/colorRef.js +78 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +45 -0
- package/dist/themes/palette.js +71 -0
- package/dist/themes/ramp.d.ts +70 -0
- package/dist/themes/ramp.js +103 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +105 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +45 -0
- package/dist/widgets/button.js +177 -0
- package/dist/widgets/checkbox.d.ts +38 -0
- package/dist/widgets/checkbox.js +135 -0
- package/dist/widgets/dropdown.d.ts +64 -0
- package/dist/widgets/dropdown.js +416 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +54 -0
- package/dist/widgets/slider.js +247 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +283 -0
- package/dist/widgets/text-input.js +1174 -0
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +41 -0
- package/dist/widgets/toggle.js +157 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +65 -0
- package/dist/widgets/widget-app.js +206 -0
- package/dist/widgets/widget-base.d.ts +42 -0
- package/dist/widgets/widget-base.js +172 -0
- package/package.json +16 -2
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 {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-html — recorded segments drawn as HTML: a fragment that can sit in
|
|
3
|
+
* a page this library does not own, and a standalone document around it.
|
|
4
|
+
*
|
|
5
|
+
* An encoding of `export-lines` and nothing more. What a run looks like under
|
|
6
|
+
* a theme is decided there; this module decides only how CSS says it. It never
|
|
7
|
+
* sees a `Style`, so it cannot disagree with the SVG exporter about what
|
|
8
|
+
* `reverse` or `dim` means — the two old converters that did disagree were
|
|
9
|
+
* deleted when this replaced them.
|
|
10
|
+
*
|
|
11
|
+
* The rows are already laid out at console width, so `pre` never re-wraps
|
|
12
|
+
* them: `white-space: pre`, and a scrollbar rather than a reflow when the page
|
|
13
|
+
* is narrower than the terminal was.
|
|
14
|
+
*/
|
|
15
|
+
import type { TerminalTheme } from "./color.js";
|
|
16
|
+
import type { Segment } from "./segment.js";
|
|
17
|
+
/**
|
|
18
|
+
* The CSS a page includes once, wherever fragments appear: what an inline
|
|
19
|
+
* style cannot say. A fragment that blinks draws steadily without it.
|
|
20
|
+
*
|
|
21
|
+
* The keyframes exist only for a reader who has not asked for reduced motion,
|
|
22
|
+
* so for one who has, the inline `animation` names nothing and the glyph holds
|
|
23
|
+
* still. One rule, so no second rule's order or survival in a host's CSS
|
|
24
|
+
* pipeline can bring the blink back.
|
|
25
|
+
*/
|
|
26
|
+
export declare const HTML_FRAGMENT_CSS = "@media (prefers-reduced-motion:no-preference){@keyframes rich-blink{50%{color:transparent}}}";
|
|
27
|
+
/**
|
|
28
|
+
* `segments` under `theme` as one `pre` carrying its canvas, for embedding.
|
|
29
|
+
*
|
|
30
|
+
* [LAW:locality-or-seam] Every rule is inline on the `pre` or below it, so the
|
|
31
|
+
* fragment styles nothing outside itself; the host page keeps its own `body`,
|
|
32
|
+
* `pre` and `a` rules. The seam holds the other way too: `all:initial` stops
|
|
33
|
+
* the host's `pre` rules and inherited typography from reaching the rows, and
|
|
34
|
+
* the two properties `all` does not cover pin the rows left to right.
|
|
35
|
+
*
|
|
36
|
+
* The one way in is the `--rich-fragment-font` custom property, read as the
|
|
37
|
+
* `font` shorthand (`14px/1.3 "JetBrains Mono", monospace`). `all` resets no
|
|
38
|
+
* custom property, so a host sets it on any ancestor to draw the rows in its
|
|
39
|
+
* own code font; unset, the rows are the browser's default monospace at a
|
|
40
|
+
* fixed line height, as a terminal's rows are. A host's value wants one too: at
|
|
41
|
+
* `normal`, a row holding a glyph from a taller fallback font grows and shifts
|
|
42
|
+
* every row below it. It must be a whole shorthand, a size and a family at
|
|
43
|
+
* least: any other value is invalid when computed, and the rows then inherit
|
|
44
|
+
* the host's font.
|
|
45
|
+
*
|
|
46
|
+
* A browser draws no line for a newline at either edge of a `pre`: the parser
|
|
47
|
+
* drops the one straight after the open tag, and the one before `</pre>` ends a
|
|
48
|
+
* line without starting another. So the fragment opens with a newline of its
|
|
49
|
+
* own and every row ends with one, and a blank first or last row is still drawn.
|
|
50
|
+
*/
|
|
51
|
+
export declare function encodeHtmlFragment(segments: Iterable<Segment>, theme?: TerminalTheme): string;
|
|
52
|
+
/**
|
|
53
|
+
* `segments` under `theme` as a complete HTML document.
|
|
54
|
+
*
|
|
55
|
+
* [LAW:one-source-of-truth] The document is the fragment in a shell, so the
|
|
56
|
+
* two cannot draw different pictures. The shell paints the page around the
|
|
57
|
+
* `pre` in the same canvas and includes the fragment CSS once.
|
|
58
|
+
*/
|
|
59
|
+
export declare function encodeHtml(segments: Iterable<Segment>, theme?: TerminalTheme): string;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-html — recorded segments drawn as HTML: a fragment that can sit in
|
|
3
|
+
* a page this library does not own, and a standalone document around it.
|
|
4
|
+
*
|
|
5
|
+
* An encoding of `export-lines` and nothing more. What a run looks like under
|
|
6
|
+
* a theme is decided there; this module decides only how CSS says it. It never
|
|
7
|
+
* sees a `Style`, so it cannot disagree with the SVG exporter about what
|
|
8
|
+
* `reverse` or `dim` means — the two old converters that did disagree were
|
|
9
|
+
* deleted when this replaced them.
|
|
10
|
+
*
|
|
11
|
+
* The rows are already laid out at console width, so `pre` never re-wraps
|
|
12
|
+
* them: `white-space: pre`, and a scrollbar rather than a reflow when the page
|
|
13
|
+
* is narrower than the terminal was.
|
|
14
|
+
*/
|
|
15
|
+
import { cellLen, graphemes } from "./cells.js";
|
|
16
|
+
import { escapeAttribute, escapeText, exportCanvas, exportLines, } from "./export-lines.js";
|
|
17
|
+
const BLINK_KEYFRAMES = "rich-blink";
|
|
18
|
+
// [LAW:dataflow-not-control-flow] Each enum-valued field of a look is a table
|
|
19
|
+
// from its values to the declarations it adds, so every value is spelled once
|
|
20
|
+
// and `Record` makes a new value a compile error until it has a row.
|
|
21
|
+
// A double underline adds no line to the glyph's own span; `runHtml` draws it.
|
|
22
|
+
const UNDERLINE_LINE = {
|
|
23
|
+
none: [],
|
|
24
|
+
single: ["underline"],
|
|
25
|
+
double: [],
|
|
26
|
+
};
|
|
27
|
+
const BLINK = {
|
|
28
|
+
none: [],
|
|
29
|
+
slow: [`animation:${BLINK_KEYFRAMES} 1s step-end infinite`],
|
|
30
|
+
fast: [`animation:${BLINK_KEYFRAMES} 0.5s step-end infinite`],
|
|
31
|
+
};
|
|
32
|
+
const FRAME = "box-shadow:inset 0 0 0 1px currentColor";
|
|
33
|
+
const OUTLINE = {
|
|
34
|
+
none: [],
|
|
35
|
+
frame: [FRAME],
|
|
36
|
+
encircle: [FRAME, "border-radius:0.5em"],
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The glyph colour, and the background when one is painted.
|
|
40
|
+
*
|
|
41
|
+
* Dim arrives already blended into `foreground`. `opacity` is never written:
|
|
42
|
+
* it fades a painted background along with the glyph, which is the bug the
|
|
43
|
+
* blend exists to avoid.
|
|
44
|
+
*/
|
|
45
|
+
function paintCss(look) {
|
|
46
|
+
return [
|
|
47
|
+
`color:${look.foreground.hex}`,
|
|
48
|
+
...(look.background === "canvas" ? [] : [`background-color:${look.background.hex}`]),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/** What is drawn on the glyph: weight, slant, single-style lines, blink, outline. */
|
|
52
|
+
function glyphCss(look) {
|
|
53
|
+
const lines = [
|
|
54
|
+
...UNDERLINE_LINE[look.underline],
|
|
55
|
+
...(look.strike ? ["line-through"] : []),
|
|
56
|
+
...(look.overline ? ["overline"] : []),
|
|
57
|
+
];
|
|
58
|
+
return [
|
|
59
|
+
...(look.bold ? ["font-weight:bold"] : []),
|
|
60
|
+
...(look.italic ? ["font-style:italic"] : []),
|
|
61
|
+
...(lines.length === 0 ? [] : [`text-decoration-line:${lines.join(" ")}`]),
|
|
62
|
+
...BLINK[look.blink],
|
|
63
|
+
...OUTLINE[look.outline],
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
// `unset` hands the anchor the run's look and drops the host's `a` rules;
|
|
67
|
+
// `revert` gives back the browser's link cursor and focus ring.
|
|
68
|
+
const ANCHOR_CSS = "all:unset;cursor:revert;outline:revert";
|
|
69
|
+
const span = (css, content) => `<span style="${escapeAttribute(css.join(";"))}">${content}</span>`;
|
|
70
|
+
/** Text of printable ASCII only: one cell a character, nothing to box. */
|
|
71
|
+
const ONE_CELL = /^[\x20-\x7e]*$/;
|
|
72
|
+
/**
|
|
73
|
+
* A box exactly `cells` of the monospace font wide (`ch` is one of its cells)
|
|
74
|
+
* for glyphs the browser would otherwise draw at a fallback font's width,
|
|
75
|
+
* which for a CJK character is rarely two cells: left to it, everything after
|
|
76
|
+
* them on the row shifts off its column. The box carries no paint, so the
|
|
77
|
+
* run's background runs through it at the run's own height; an atomic inline
|
|
78
|
+
* takes no decoration from its ancestors, so it inherits its parent's lines.
|
|
79
|
+
*/
|
|
80
|
+
const cellBox = (cells) => ["display:inline-block", `width:${cells}ch`, "text-align:center", "text-decoration:inherit"];
|
|
81
|
+
/**
|
|
82
|
+
* `text` with each stretch of graphemes wider than one cell in one box as wide
|
|
83
|
+
* as their cells together, and `draw` applied to every stretch of escaped text
|
|
84
|
+
* inside and between the boxes. A stretch, not a glyph apiece, because a
|
|
85
|
+
* browser's find-in-page does not match across two boxes. Graphemes, not code
|
|
86
|
+
* points, because `cellLen` measures a joined emoji as one glyph; a zero-width
|
|
87
|
+
* one joins the stretch before it, so it can still combine.
|
|
88
|
+
*/
|
|
89
|
+
function onGrid(text, draw) {
|
|
90
|
+
if (ONE_CELL.test(text))
|
|
91
|
+
return draw(escapeText(text));
|
|
92
|
+
const pieces = [];
|
|
93
|
+
let stretch = "";
|
|
94
|
+
let wideCells = 0;
|
|
95
|
+
const flush = () => {
|
|
96
|
+
if (stretch !== "")
|
|
97
|
+
pieces.push(wideCells === 0 ? draw(escapeText(stretch)) : span(cellBox(wideCells), draw(escapeText(stretch))));
|
|
98
|
+
stretch = "";
|
|
99
|
+
wideCells = 0;
|
|
100
|
+
};
|
|
101
|
+
for (const segment of graphemes(text)) {
|
|
102
|
+
const cells = cellLen(segment);
|
|
103
|
+
const wide = cells === 0 ? wideCells > 0 : cells > 1;
|
|
104
|
+
if (wide !== (wideCells > 0))
|
|
105
|
+
flush();
|
|
106
|
+
stretch += segment;
|
|
107
|
+
if (wide)
|
|
108
|
+
wideCells += cells;
|
|
109
|
+
}
|
|
110
|
+
flush();
|
|
111
|
+
return pieces.join("");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* One run as markup.
|
|
115
|
+
*
|
|
116
|
+
* An element has one `text-decoration-style`, so a double underline sharing a
|
|
117
|
+
* span with a strike would double the strike too. It gets an outer span of its
|
|
118
|
+
* own instead. That span carries the paint, because a descendant's background
|
|
119
|
+
* may be painted over an ancestor's underline, and the blink, so the underline
|
|
120
|
+
* blinks with the glyph. A wide glyph's box sits between the two: it inherits
|
|
121
|
+
* the double underline, and the inner span inside it draws the other lines.
|
|
122
|
+
*/
|
|
123
|
+
function runHtml({ text, look }) {
|
|
124
|
+
const drawn = look.underline === "double"
|
|
125
|
+
? span([...paintCss(look), "text-decoration-line:underline", "text-decoration-style:double", ...BLINK[look.blink]], onGrid(text, (glyph) => span([`color:${look.foreground.hex}`, ...glyphCss(look)], glyph)))
|
|
126
|
+
: span([...paintCss(look), ...glyphCss(look)], onGrid(text, (glyph) => glyph));
|
|
127
|
+
return look.href === null
|
|
128
|
+
? drawn
|
|
129
|
+
: `<a href="${escapeAttribute(look.href)}" style="${ANCHOR_CSS}">${drawn}</a>`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The CSS a page includes once, wherever fragments appear: what an inline
|
|
133
|
+
* style cannot say. A fragment that blinks draws steadily without it.
|
|
134
|
+
*
|
|
135
|
+
* The keyframes exist only for a reader who has not asked for reduced motion,
|
|
136
|
+
* so for one who has, the inline `animation` names nothing and the glyph holds
|
|
137
|
+
* still. One rule, so no second rule's order or survival in a host's CSS
|
|
138
|
+
* pipeline can bring the blink back.
|
|
139
|
+
*/
|
|
140
|
+
export const HTML_FRAGMENT_CSS = `@media (prefers-reduced-motion:no-preference){@keyframes ${BLINK_KEYFRAMES}{50%{color:transparent}}}`;
|
|
141
|
+
/**
|
|
142
|
+
* `segments` under `theme` as one `pre` carrying its canvas, for embedding.
|
|
143
|
+
*
|
|
144
|
+
* [LAW:locality-or-seam] Every rule is inline on the `pre` or below it, so the
|
|
145
|
+
* fragment styles nothing outside itself; the host page keeps its own `body`,
|
|
146
|
+
* `pre` and `a` rules. The seam holds the other way too: `all:initial` stops
|
|
147
|
+
* the host's `pre` rules and inherited typography from reaching the rows, and
|
|
148
|
+
* the two properties `all` does not cover pin the rows left to right.
|
|
149
|
+
*
|
|
150
|
+
* The one way in is the `--rich-fragment-font` custom property, read as the
|
|
151
|
+
* `font` shorthand (`14px/1.3 "JetBrains Mono", monospace`). `all` resets no
|
|
152
|
+
* custom property, so a host sets it on any ancestor to draw the rows in its
|
|
153
|
+
* own code font; unset, the rows are the browser's default monospace at a
|
|
154
|
+
* fixed line height, as a terminal's rows are. A host's value wants one too: at
|
|
155
|
+
* `normal`, a row holding a glyph from a taller fallback font grows and shifts
|
|
156
|
+
* every row below it. It must be a whole shorthand, a size and a family at
|
|
157
|
+
* least: any other value is invalid when computed, and the rows then inherit
|
|
158
|
+
* the host's font.
|
|
159
|
+
*
|
|
160
|
+
* A browser draws no line for a newline at either edge of a `pre`: the parser
|
|
161
|
+
* drops the one straight after the open tag, and the one before `</pre>` ends a
|
|
162
|
+
* line without starting another. So the fragment opens with a newline of its
|
|
163
|
+
* own and every row ends with one, and a blank first or last row is still drawn.
|
|
164
|
+
*/
|
|
165
|
+
export function encodeHtmlFragment(segments, theme) {
|
|
166
|
+
const canvas = exportCanvas(theme);
|
|
167
|
+
const rows = exportLines(segments, theme).map((row) => `${row.map(runHtml).join("")}\n`);
|
|
168
|
+
const css = [
|
|
169
|
+
"all:initial",
|
|
170
|
+
"direction:ltr",
|
|
171
|
+
"unicode-bidi:isolate",
|
|
172
|
+
"display:block",
|
|
173
|
+
`background:${canvas.background.hex}`,
|
|
174
|
+
`color:${canvas.foreground.hex}`,
|
|
175
|
+
"padding:1em",
|
|
176
|
+
"font:var(--rich-fragment-font,medium/1.2 monospace)",
|
|
177
|
+
"white-space:pre",
|
|
178
|
+
"overflow-x:auto",
|
|
179
|
+
].join(";");
|
|
180
|
+
return `<pre style="${css}">\n${rows.join("")}</pre>`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `segments` under `theme` as a complete HTML document.
|
|
184
|
+
*
|
|
185
|
+
* [LAW:one-source-of-truth] The document is the fragment in a shell, so the
|
|
186
|
+
* two cannot draw different pictures. The shell paints the page around the
|
|
187
|
+
* `pre` in the same canvas and includes the fragment CSS once.
|
|
188
|
+
*/
|
|
189
|
+
export function encodeHtml(segments, theme) {
|
|
190
|
+
const canvas = exportCanvas(theme);
|
|
191
|
+
const css = `body{background:${canvas.background.hex};color:${canvas.foreground.hex};margin:0}\n${HTML_FRAGMENT_CSS}`;
|
|
192
|
+
return [
|
|
193
|
+
"<!DOCTYPE html>",
|
|
194
|
+
`<html><head><meta charset="utf-8"><style>\n${css}\n</style></head>`,
|
|
195
|
+
`<body>${encodeHtmlFragment(segments, theme)}</body></html>`,
|
|
196
|
+
].join("\n");
|
|
197
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-lines — recorded segments resolved against a `TerminalTheme` into the
|
|
3
|
+
* lines every exporter draws.
|
|
4
|
+
*
|
|
5
|
+
* The HTML and SVG exporters are two encodings of one picture, and this module
|
|
6
|
+
* is the picture. What a styled cell *looks like* — which colour its glyph is,
|
|
7
|
+
* whether its background is painted, which decorations it carries, where it
|
|
8
|
+
* links — is decided here, once, and an exporter decides only how to write
|
|
9
|
+
* that down. Before it existed the decision was made twice, by a CSS method
|
|
10
|
+
* on `Style` and by a private converter on `Console`, and the two had already
|
|
11
|
+
* disagreed: one honoured `underline2`, the other dropped it.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:types-are-the-program] `ExportLook` has no `reverse`, `dim`, `conceal`
|
|
14
|
+
* or default colour, because all four are consumed by `resolveLook` and turned
|
|
15
|
+
* into plain RGB. An exporter holding a look cannot forget to swap a reversed
|
|
16
|
+
* run or to fade a dim one: there is nothing left in its hands to forget. That
|
|
17
|
+
* is the whole reason the representation is resolved rather than a `Style`
|
|
18
|
+
* handed through.
|
|
19
|
+
*
|
|
20
|
+
* Runs rather than a cell grid. A grid gives a wide character a phantom
|
|
21
|
+
* continuation cell every consumer must know to skip, and gives an HTML
|
|
22
|
+
* exporter — which lays text out by flowing it — nothing it needs. A run's
|
|
23
|
+
* column and cell width are `cellLen` of the text before it and of its own
|
|
24
|
+
* text; they are not stored, so they cannot disagree with the text
|
|
25
|
+
* [LAW:one-source-of-truth]. The SVG exporter, the one consumer that positions
|
|
26
|
+
* anything, derives them.
|
|
27
|
+
*
|
|
28
|
+
* Browser-safe by construction: this imports `color`, `style`, `segment` and
|
|
29
|
+
* nothing that touches the host, so `Console` can reach it from the main
|
|
30
|
+
* barrel and the fs half of exporting stays in `src/node/save.ts`.
|
|
31
|
+
*/
|
|
32
|
+
import { type ColorRgba, type TerminalTheme } from "./color.js";
|
|
33
|
+
import { type Style } from "./style.js";
|
|
34
|
+
import type { Segment } from "./segment.js";
|
|
35
|
+
declare const _href: unique symbol;
|
|
36
|
+
/**
|
|
37
|
+
* A link target that is safe to write into a published document.
|
|
38
|
+
*
|
|
39
|
+
* [LAW:parse-dont-validate] Only `parseHref` produces one, so an exporter that
|
|
40
|
+
* writes an `href` attribute from an `Href` cannot have skipped the check.
|
|
41
|
+
*/
|
|
42
|
+
export type Href = string & {
|
|
43
|
+
readonly [_href]: true;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Where a run's background comes from. `"canvas"` is the theme background
|
|
47
|
+
* showing through — nothing painted — and is distinct from a painted colour
|
|
48
|
+
* that happens to equal it, because `on default` and `on #000000` are
|
|
49
|
+
* different requests even over a black theme.
|
|
50
|
+
*/
|
|
51
|
+
export type Background = ColorRgba | "canvas";
|
|
52
|
+
/** Everything an exporter needs to draw one run, and nothing left to resolve. */
|
|
53
|
+
export interface ExportLook {
|
|
54
|
+
readonly foreground: ColorRgba;
|
|
55
|
+
readonly background: Background;
|
|
56
|
+
readonly bold: boolean;
|
|
57
|
+
readonly italic: boolean;
|
|
58
|
+
readonly underline: "none" | "single" | "double";
|
|
59
|
+
readonly strike: boolean;
|
|
60
|
+
readonly overline: boolean;
|
|
61
|
+
readonly blink: "none" | "slow" | "fast";
|
|
62
|
+
readonly outline: "none" | "frame" | "encircle";
|
|
63
|
+
readonly href: Href | null;
|
|
64
|
+
}
|
|
65
|
+
export interface ExportRun {
|
|
66
|
+
readonly text: string;
|
|
67
|
+
readonly look: ExportLook;
|
|
68
|
+
}
|
|
69
|
+
/** One terminal row, left to right. An empty array is a blank row. */
|
|
70
|
+
export type ExportLine = readonly ExportRun[];
|
|
71
|
+
/**
|
|
72
|
+
* `Style.link` as a link an export may write, or `null` when it may not.
|
|
73
|
+
*
|
|
74
|
+
* A refused link loses only its target: the run keeps its text and every other
|
|
75
|
+
* attribute, so nothing the terminal showed disappears from the export. The
|
|
76
|
+
* canonical `URL.href` is what comes back, since the parse is what vouches for
|
|
77
|
+
* it.
|
|
78
|
+
*/
|
|
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;
|
|
88
|
+
/** What an unstyled cell shows under a theme: the canvas, and ink on it. */
|
|
89
|
+
export interface ExportCanvas {
|
|
90
|
+
readonly background: ColorRgba;
|
|
91
|
+
readonly foreground: ColorRgba;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The ground every export is drawn on under `theme`.
|
|
95
|
+
*
|
|
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.
|
|
99
|
+
*/
|
|
100
|
+
export declare function exportCanvas(theme?: TerminalTheme): ExportCanvas;
|
|
101
|
+
/**
|
|
102
|
+
* A `Style` as it appears on screen under `theme`.
|
|
103
|
+
*
|
|
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.
|
|
110
|
+
*
|
|
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.
|
|
116
|
+
*
|
|
117
|
+
* `theme` omitted is `ColorSpec.getTruecolor`'s own fallback — black canvas,
|
|
118
|
+
* white ink, the standard ANSI table. Choosing it there rather than naming a
|
|
119
|
+
* preset here is what keeps `core/` from a third upward edge into `themes/`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function resolveLook(style: Style, theme?: TerminalTheme): ExportLook;
|
|
122
|
+
/**
|
|
123
|
+
* Recorded segments as terminal rows under `theme`.
|
|
124
|
+
*
|
|
125
|
+
* `"\n"` ends a row, so output that ends with a newline — every `print` does —
|
|
126
|
+
* has no empty row after it, and `"a\n\nb"` keeps its blank row. Callers pass
|
|
127
|
+
* the recording buffer, which already holds no control segments.
|
|
128
|
+
*/
|
|
129
|
+
export declare function exportLines(segments: Iterable<Segment>, theme?: TerminalTheme): ExportLine[];
|
|
130
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-lines — recorded segments resolved against a `TerminalTheme` into the
|
|
3
|
+
* lines every exporter draws.
|
|
4
|
+
*
|
|
5
|
+
* The HTML and SVG exporters are two encodings of one picture, and this module
|
|
6
|
+
* is the picture. What a styled cell *looks like* — which colour its glyph is,
|
|
7
|
+
* whether its background is painted, which decorations it carries, where it
|
|
8
|
+
* links — is decided here, once, and an exporter decides only how to write
|
|
9
|
+
* that down. Before it existed the decision was made twice, by a CSS method
|
|
10
|
+
* on `Style` and by a private converter on `Console`, and the two had already
|
|
11
|
+
* disagreed: one honoured `underline2`, the other dropped it.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:types-are-the-program] `ExportLook` has no `reverse`, `dim`, `conceal`
|
|
14
|
+
* or default colour, because all four are consumed by `resolveLook` and turned
|
|
15
|
+
* into plain RGB. An exporter holding a look cannot forget to swap a reversed
|
|
16
|
+
* run or to fade a dim one: there is nothing left in its hands to forget. That
|
|
17
|
+
* is the whole reason the representation is resolved rather than a `Style`
|
|
18
|
+
* handed through.
|
|
19
|
+
*
|
|
20
|
+
* Runs rather than a cell grid. A grid gives a wide character a phantom
|
|
21
|
+
* continuation cell every consumer must know to skip, and gives an HTML
|
|
22
|
+
* exporter — which lays text out by flowing it — nothing it needs. A run's
|
|
23
|
+
* column and cell width are `cellLen` of the text before it and of its own
|
|
24
|
+
* text; they are not stored, so they cannot disagree with the text
|
|
25
|
+
* [LAW:one-source-of-truth]. The SVG exporter, the one consumer that positions
|
|
26
|
+
* anything, derives them.
|
|
27
|
+
*
|
|
28
|
+
* Browser-safe by construction: this imports `color`, `style`, `segment` and
|
|
29
|
+
* nothing that touches the host, so `Console` can reach it from the main
|
|
30
|
+
* barrel and the fs half of exporting stays in `src/node/save.ts`.
|
|
31
|
+
*/
|
|
32
|
+
import { blendRgb, ColorSpec } from "./color.js";
|
|
33
|
+
import { NULL_STYLE } from "./style.js";
|
|
34
|
+
/**
|
|
35
|
+
* The URL schemes an export will link.
|
|
36
|
+
*
|
|
37
|
+
* A terminal hands an OSC 8 target to the user's opener; an exported page hands
|
|
38
|
+
* it to whoever loads the page, and exports are made to be published. A
|
|
39
|
+
* `javascript:` target that was inert in a terminal is script in a README. The
|
|
40
|
+
* list is an allowance, not a denylist, so a scheme nobody considered is
|
|
41
|
+
* refused rather than trusted.
|
|
42
|
+
*/
|
|
43
|
+
const LINKABLE_SCHEMES = new Set(["http:", "https:", "mailto:", "ftp:", "file:"]);
|
|
44
|
+
/**
|
|
45
|
+
* `Style.link` as a link an export may write, or `null` when it may not.
|
|
46
|
+
*
|
|
47
|
+
* A refused link loses only its target: the run keeps its text and every other
|
|
48
|
+
* attribute, so nothing the terminal showed disappears from the export. The
|
|
49
|
+
* canonical `URL.href` is what comes back, since the parse is what vouches for
|
|
50
|
+
* it.
|
|
51
|
+
*/
|
|
52
|
+
export function parseHref(link) {
|
|
53
|
+
if (!URL.canParse(link))
|
|
54
|
+
return null;
|
|
55
|
+
const url = new URL(link);
|
|
56
|
+
return LINKABLE_SCHEMES.has(url.protocol) ? url.href : null;
|
|
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]);
|
|
74
|
+
/**
|
|
75
|
+
* How far a dim glyph moves toward its background — Rich's own factor, so an
|
|
76
|
+
* export of the same program matches the library this ports.
|
|
77
|
+
*/
|
|
78
|
+
const DIM_FADE = 0.4;
|
|
79
|
+
const DEFAULT_COLOR = ColorSpec.default();
|
|
80
|
+
/**
|
|
81
|
+
* The ground every export is drawn on under `theme`.
|
|
82
|
+
*
|
|
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.
|
|
86
|
+
*/
|
|
87
|
+
export function exportCanvas(theme) {
|
|
88
|
+
return {
|
|
89
|
+
background: DEFAULT_COLOR.getTruecolor(theme, false),
|
|
90
|
+
foreground: DEFAULT_COLOR.getTruecolor(theme, true),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A `Style` as it appears on screen under `theme`.
|
|
95
|
+
*
|
|
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.
|
|
102
|
+
*
|
|
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.
|
|
108
|
+
*
|
|
109
|
+
* `theme` omitted is `ColorSpec.getTruecolor`'s own fallback — black canvas,
|
|
110
|
+
* white ink, the standard ANSI table. Choosing it there rather than naming a
|
|
111
|
+
* preset here is what keeps `core/` from a third upward edge into `themes/`.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveLook(style, theme) {
|
|
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);
|
|
121
|
+
// A reversed run always paints: its background is the ink, which is never
|
|
122
|
+
// the canvas, even when the ink is the theme's default foreground.
|
|
123
|
+
const glyph = style.reverse ? paper : ink;
|
|
124
|
+
const ground = style.reverse ? ink : paper;
|
|
125
|
+
const background = style.reverse || !paperSpec.isDefault ? ground : "canvas";
|
|
126
|
+
const faded = style.dim ? blendRgb(glyph, ground, DIM_FADE) : glyph;
|
|
127
|
+
return {
|
|
128
|
+
foreground: style.conceal ? ground : faded,
|
|
129
|
+
background,
|
|
130
|
+
bold: style.bold === true,
|
|
131
|
+
italic: style.italic === true,
|
|
132
|
+
underline: style.underline2 ? "double" : style.underline ? "single" : "none",
|
|
133
|
+
strike: style.strike === true,
|
|
134
|
+
overline: style.overline === true,
|
|
135
|
+
blink: style.blink2 ? "fast" : style.blink ? "slow" : "none",
|
|
136
|
+
outline: style.encircle ? "encircle" : style.frame ? "frame" : "none",
|
|
137
|
+
href: style.link === undefined ? null : parseHref(style.link),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Recorded segments as terminal rows under `theme`.
|
|
142
|
+
*
|
|
143
|
+
* `"\n"` ends a row, so output that ends with a newline — every `print` does —
|
|
144
|
+
* has no empty row after it, and `"a\n\nb"` keeps its blank row. Callers pass
|
|
145
|
+
* the recording buffer, which already holds no control segments.
|
|
146
|
+
*/
|
|
147
|
+
export function exportLines(segments, theme) {
|
|
148
|
+
const rows = [[]];
|
|
149
|
+
for (const segment of segments) {
|
|
150
|
+
const look = resolveLook(segment.style ?? NULL_STYLE, theme);
|
|
151
|
+
segment.text.split("\n").forEach((piece, index) => {
|
|
152
|
+
if (index > 0)
|
|
153
|
+
rows.push([]);
|
|
154
|
+
if (piece.length > 0)
|
|
155
|
+
rows[rows.length - 1].push({ text: piece, look });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return rows[rows.length - 1].length === 0 ? rows.slice(0, -1) : rows;
|
|
159
|
+
}
|
|
@@ -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;
|