@promptctl/rich-js 0.18.0 → 0.18.1
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/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 +143 -0
- package/dist/core/box.js +379 -0
- package/dist/core/cells.d.ts +159 -0
- package/dist/core/cells.js +287 -0
- package/dist/core/color.d.ts +246 -0
- package/dist/core/color.js +1038 -0
- package/dist/core/console.d.ts +165 -0
- package/dist/core/console.js +527 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +200 -0
- package/dist/core/export-lines.d.ts +121 -0
- package/dist/core/export-lines.js +141 -0
- package/dist/core/highlighter.d.ts +33 -0
- package/dist/core/highlighter.js +124 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +119 -0
- package/dist/core/markup.js +731 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +62 -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 +90 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +144 -0
- package/dist/core/pretty.js +476 -0
- package/dist/core/protocol.d.ts +267 -0
- package/dist/core/protocol.js +163 -0
- package/dist/core/render.d.ts +84 -0
- package/dist/core/render.js +145 -0
- package/dist/core/segment.d.ts +161 -0
- package/dist/core/segment.js +492 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +141 -0
- package/dist/core/strip.js +305 -0
- package/dist/core/style.d.ts +169 -0
- package/dist/core/style.js +695 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +256 -0
- package/dist/core/text.js +976 -0
- package/dist/core/wrap.d.ts +41 -0
- package/dist/core/wrap.js +127 -0
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +179 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +107 -0
- package/dist/node/prompt.d.ts +21 -0
- package/dist/node/prompt.js +30 -0
- package/dist/node/save.d.ts +17 -0
- package/dist/node/save.js +21 -0
- package/dist/node/terminal-host.d.ts +113 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +37 -0
- package/dist/node/traceback.js +134 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +33 -0
- package/dist/renderables/columns.d.ts +61 -0
- package/dist/renderables/columns.js +152 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +37 -0
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +11 -0
- package/dist/renderables/group.js +17 -0
- package/dist/renderables/layout.d.ts +92 -0
- package/dist/renderables/layout.js +312 -0
- package/dist/renderables/live.d.ts +50 -0
- package/dist/renderables/live.js +175 -0
- package/dist/renderables/markdown.d.ts +24 -0
- package/dist/renderables/markdown.js +184 -0
- package/dist/renderables/padding.d.ts +38 -0
- package/dist/renderables/padding.js +141 -0
- package/dist/renderables/panel.d.ts +116 -0
- package/dist/renderables/panel.js +307 -0
- package/dist/renderables/progress.d.ts +90 -0
- package/dist/renderables/progress.js +238 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +53 -0
- package/dist/renderables/prompt.d.ts +49 -0
- package/dist/renderables/prompt.js +125 -0
- package/dist/renderables/rule.d.ts +25 -0
- package/dist/renderables/rule.js +90 -0
- package/dist/renderables/spinner.d.ts +31 -0
- package/dist/renderables/spinner.js +70 -0
- package/dist/renderables/status.d.ts +23 -0
- package/dist/renderables/status.js +60 -0
- package/dist/renderables/syntax.d.ts +35 -0
- package/dist/renderables/syntax.js +115 -0
- package/dist/renderables/table.d.ts +194 -0
- package/dist/renderables/table.js +795 -0
- package/dist/renderables/traceback.d.ts +31 -0
- package/dist/renderables/traceback.js +108 -0
- package/dist/renderables/tree.d.ts +68 -0
- package/dist/renderables/tree.js +150 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.d.ts +87 -0
- package/dist/template-bindings/color-funcs.js +217 -0
- package/dist/template-bindings/helpers.d.ts +24 -0
- package/dist/template-bindings/helpers.js +48 -0
- package/dist/template-bindings/index.d.ts +107 -0
- package/dist/template-bindings/index.js +146 -0
- package/dist/template-bindings/palette-funcs.d.ts +94 -0
- package/dist/template-bindings/palette-funcs.js +141 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +155 -0
- package/dist/themes/buildPalette.d.ts +36 -0
- package/dist/themes/buildPalette.js +56 -0
- package/dist/themes/colorMath.d.ts +82 -0
- package/dist/themes/colorMath.js +254 -0
- package/dist/themes/colorRef.d.ts +51 -0
- package/dist/themes/colorRef.js +96 -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 +21 -0
- package/dist/themes/palette.js +28 -0
- package/dist/themes/ramp.d.ts +83 -0
- package/dist/themes/ramp.js +122 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +94 -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 +49 -0
- package/dist/widgets/button.js +208 -0
- package/dist/widgets/checkbox.d.ts +41 -0
- package/dist/widgets/checkbox.js +157 -0
- package/dist/widgets/dropdown.d.ts +67 -0
- package/dist/widgets/dropdown.js +450 -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/slider.d.ts +57 -0
- package/dist/widgets/slider.js +270 -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 +274 -0
- package/dist/widgets/text-input.js +1165 -0
- package/dist/widgets/toggle.d.ts +44 -0
- package/dist/widgets/toggle.js +180 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +40 -0
- package/dist/widgets/widget-base.js +165 -0
- package/package.json +6 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering protocol interfaces — Renderable, Measurable, RenderOptions.
|
|
3
|
+
* [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
|
|
4
|
+
*/
|
|
5
|
+
import type { Segment } from "./segment.js";
|
|
6
|
+
import { type Style, type StyleSyntaxError, type Theme } from "./style.js";
|
|
7
|
+
import type { ColorDepth } from "./color.js";
|
|
8
|
+
/**
|
|
9
|
+
* What becomes of a line still wider than its width once wrapping is done.
|
|
10
|
+
* `"fold"` breaks it across lines, `"crop"` cuts it at the edge, `"ellipsis"`
|
|
11
|
+
* cuts it and marks the cut. `"ignore"` is not a way of cutting: the line has
|
|
12
|
+
* no edge, so nothing wraps it and nothing cuts it, and it leaves at its
|
|
13
|
+
* natural width for whatever drew it to crop or not.
|
|
14
|
+
*
|
|
15
|
+
* `noWrap` is the other half of Rich's pair and is not `"ignore"`: it stops the
|
|
16
|
+
* wrapping and leaves the edge, so a line too wide is cut by the method here.
|
|
17
|
+
*/
|
|
18
|
+
export type OverflowMethod = "fold" | "crop" | "ellipsis" | "ignore";
|
|
19
|
+
export interface RenderOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The cells this renderable may occupy. A count of cells, so a non-negative
|
|
22
|
+
* integer — but the field is a plain `number` because it is public API and a
|
|
23
|
+
* caller can write anything into it. `withCellWidth` is where that becomes
|
|
24
|
+
* true rather than assumed; see its comment for why every renderable must
|
|
25
|
+
* call it rather than trusting this declaration.
|
|
26
|
+
*/
|
|
27
|
+
maxWidth: number;
|
|
28
|
+
minWidth?: number;
|
|
29
|
+
/**
|
|
30
|
+
* The rows this renderable may occupy. Absent, nothing limits them.
|
|
31
|
+
*/
|
|
32
|
+
height?: Height;
|
|
33
|
+
isTerminal?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The output can draw only ASCII. Every glyph a renderable chooses — box
|
|
36
|
+
* lines, tree guides, a check mark — is resolved through `drawable`. Text
|
|
37
|
+
* the caller hands in is drawn as given. Absent means the output draws any
|
|
38
|
+
* glyph.
|
|
39
|
+
*/
|
|
40
|
+
asciiOnly?: boolean;
|
|
41
|
+
justify?: "left" | "center" | "right" | "full";
|
|
42
|
+
overflow?: OverflowMethod;
|
|
43
|
+
noWrap?: boolean;
|
|
44
|
+
highlight?: unknown;
|
|
45
|
+
markup?: unknown;
|
|
46
|
+
/**
|
|
47
|
+
* The depth the output will be encoded at, `null` when it carries no colour.
|
|
48
|
+
* A renderable choosing between two ways of drawing something — the strip's
|
|
49
|
+
* arrow or its divider — decides on the colours the terminal will draw at
|
|
50
|
+
* this depth, which at 256 colours or fewer are not the colours it was
|
|
51
|
+
* handed. Absent means truecolor.
|
|
52
|
+
*/
|
|
53
|
+
colorSystem?: ColorDepth | null;
|
|
54
|
+
/**
|
|
55
|
+
* The names a style string may use. A `Console` passes its own; absent, the
|
|
56
|
+
* built-in defaults apply. Read it through `getStyle` rather than directly.
|
|
57
|
+
*/
|
|
58
|
+
theme?: Theme;
|
|
59
|
+
/**
|
|
60
|
+
* Called each time a render degrades a style string it cannot parse to
|
|
61
|
+
* unstyled, once per resolution — a style the render reads twice is reported
|
|
62
|
+
* twice. Absent, the failure passes silently. A handler that throws makes
|
|
63
|
+
* the render strict: the error leaves `render`.
|
|
64
|
+
*/
|
|
65
|
+
onStyleError?: StyleErrorHandler;
|
|
66
|
+
/**
|
|
67
|
+
* Told of each owner that stamps its output (`core/anchor`) as it starts to
|
|
68
|
+
* render, so the calls arrive in document order: a container's children in
|
|
69
|
+
* the order it renders them, an owner before the owners nested in it. A
|
|
70
|
+
* container forwards it by passing its options on, which every container
|
|
71
|
+
* does. An owner rendered twice is told twice. A render made to measure is
|
|
72
|
+
* not drawing and is not told (`measuring`).
|
|
73
|
+
*/
|
|
74
|
+
onDraw?: (owner: object) => void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `chosen` as this output can draw it: `ascii` in its place when the output is
|
|
78
|
+
* ASCII-only and `chosen` spends a glyph outside ASCII, otherwise `chosen`
|
|
79
|
+
* itself. Choosing a glyph that is already ASCII (a rule of `=`, a MARKDOWN box)
|
|
80
|
+
* picks a frame the terminal can draw, so it is kept. A set of glyphs that must
|
|
81
|
+
* change together — a box grid, a spinner's frames, a scrollbar — passes
|
|
82
|
+
* `glyphs` to say which characters it spends, and is replaced whole.
|
|
83
|
+
*
|
|
84
|
+
* [LAW:single-enforcer] The one place that decides what `asciiOnly` replaces.
|
|
85
|
+
* `test/renderables/ascii-only.test.ts` renders every exported renderable to
|
|
86
|
+
* hold each of them to it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function drawable(options: {
|
|
89
|
+
readonly asciiOnly?: boolean;
|
|
90
|
+
}, chosen: string, ascii: string): string;
|
|
91
|
+
export declare function drawable<T>(options: {
|
|
92
|
+
readonly asciiOnly?: boolean;
|
|
93
|
+
}, chosen: T, ascii: T, glyphs: (value: T) => string): T;
|
|
94
|
+
/**
|
|
95
|
+
* A vertical budget: a count of rows, and whether they are a region the
|
|
96
|
+
* renderable stands in or a ceiling it stays under.
|
|
97
|
+
*
|
|
98
|
+
* `exact: true` is a region — a `Layout` pane, a full-screen frame. The output
|
|
99
|
+
* is exactly `rows` tall. A renderable may fill the region — a layout divides
|
|
100
|
+
* it, a frame can stretch to its last row — or ignore it and emit its natural
|
|
101
|
+
* height; either way, whoever set the region shapes what comes back to it
|
|
102
|
+
* (`fitHeight`). Setting a region is a promise to shape, so no renderable pads
|
|
103
|
+
* or crops itself to one.
|
|
104
|
+
*
|
|
105
|
+
* `exact: false` is a ceiling — the terminal an inline print lands on. Content
|
|
106
|
+
* keeps its natural height beneath it, and nothing pads up to the ceiling.
|
|
107
|
+
* Output taller than the ceiling is its setter's to handle: an inline `Live`
|
|
108
|
+
* applies its `verticalOverflow`, and a print lets the terminal scroll it.
|
|
109
|
+
*
|
|
110
|
+
* A renderable passing its whole space to one child must forward the budget
|
|
111
|
+
* less the rows it draws itself, and of the same kind (`insetHeight`). One
|
|
112
|
+
* stacking several children must hand each the rows as a ceiling
|
|
113
|
+
* (`stackedHeight`): any one of them may use all of it, and none may claim it
|
|
114
|
+
* as its region. Forwarded unchanged, a layout nested in a panel fills the
|
|
115
|
+
* panel's whole region and the region's crop takes the panel's bottom border.
|
|
116
|
+
*
|
|
117
|
+
* [LAW:types-are-the-program] One field, because the fact is one count and one
|
|
118
|
+
* bit about it. As two numbers, `height` and `maxHeight`, it admitted a region
|
|
119
|
+
* taller than its ceiling and a region with no ceiling at all, and the one
|
|
120
|
+
* reader in the library resolved them `maxHeight ?? height` — the ceiling ahead
|
|
121
|
+
* of the region it contains.
|
|
122
|
+
*
|
|
123
|
+
* There is no vertical `Measurable`. A parent chooses widths from its
|
|
124
|
+
* children's measurements before any of them renders; nothing chooses rows
|
|
125
|
+
* that way, because a renderable's height is a consequence of the width it
|
|
126
|
+
* renders at. Rendering and counting the lines is the measurement.
|
|
127
|
+
*/
|
|
128
|
+
export interface Height {
|
|
129
|
+
readonly rows: number;
|
|
130
|
+
readonly exact: boolean;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The budget a renderable hands the one child filling its space: its own, less
|
|
134
|
+
* the `rows` it draws itself, and of the same kind.
|
|
135
|
+
*/
|
|
136
|
+
export declare function insetHeight(height: Height | undefined, rows: number): Height | undefined;
|
|
137
|
+
/**
|
|
138
|
+
* The budget a renderable hands each of several children it stacks: its rows,
|
|
139
|
+
* as a ceiling.
|
|
140
|
+
*/
|
|
141
|
+
export declare function stackedHeight(height: Height | undefined): Height | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* The rows a region holds, parsed as a cell count, or `undefined` when there
|
|
144
|
+
* is no region to fill: a ceiling, no budget, or a region of `Infinity` rows,
|
|
145
|
+
* which names no count — as an unbounded width resolves to a natural one in
|
|
146
|
+
* `withBoundedWidth`.
|
|
147
|
+
*/
|
|
148
|
+
export declare function regionRows(height: Height | undefined): number | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* What came back from a child, held to the region `height` names — blank rows
|
|
151
|
+
* padded below, overflow cropped from the bottom — or left at its own height
|
|
152
|
+
* when `height` is no region. This is the shaping a region's setter owes.
|
|
153
|
+
*/
|
|
154
|
+
export declare function fitHeight(lines: Segment[][], height: Height | undefined): Segment[][];
|
|
155
|
+
/**
|
|
156
|
+
* Receives a style error a render would otherwise absorb: the parse failure,
|
|
157
|
+
* and the whole style string it came from — the error names only the token
|
|
158
|
+
* that failed, and `"bold rd"` loses its `bold` too.
|
|
159
|
+
*
|
|
160
|
+
* [LAW:no-mode-explosion] Strict mode is not a second option beside this one.
|
|
161
|
+
* It is a handler that throws, so there is no "strict and a callback" state
|
|
162
|
+
* whose order anyone has to define.
|
|
163
|
+
*/
|
|
164
|
+
export type StyleErrorHandler = (error: StyleSyntaxError, style: string) => void;
|
|
165
|
+
/**
|
|
166
|
+
* The style a `string | Style` stands for in this render: a name from the
|
|
167
|
+
* theme of whatever is drawing, otherwise a style definition. A string that is
|
|
168
|
+
* neither throws `StyleSyntaxError`. This is how a renderable that yields its
|
|
169
|
+
* own `Segment`s draws with a theme name, since a `Segment` takes a resolved
|
|
170
|
+
* `Style`. It is Rich's `console.get_style`, asked of the options because the
|
|
171
|
+
* options are where the theme travels.
|
|
172
|
+
*
|
|
173
|
+
* [LAW:single-enforcer] Every renderable resolves a style name here, at render
|
|
174
|
+
* time, because only the render knows whose theme it is drawing for. Resolved
|
|
175
|
+
* when a renderable was built instead, a name was fixed to the built-in
|
|
176
|
+
* defaults before any `Console` could offer its theme, and `new Console({
|
|
177
|
+
* theme })` changed nothing.
|
|
178
|
+
*/
|
|
179
|
+
export declare function getStyle(options: RenderOptions, style: string | Style): Style;
|
|
180
|
+
/**
|
|
181
|
+
* The options a renderable should work from: the caller's, with `maxWidth`
|
|
182
|
+
* parsed into an actual count of cells.
|
|
183
|
+
*
|
|
184
|
+
* [LAW:parse-dont-validate] The stamp has to travel, which is the whole reason
|
|
185
|
+
* this returns options rather than a number. Parsing `options.maxWidth` into a
|
|
186
|
+
* local leaves the caller's raw value sitting in `options` for whatever the
|
|
187
|
+
* renderable forwards it to — and every renderable forwards it, to a child's
|
|
188
|
+
* `render`, to `Measurement.get`, to a nested layout. `Columns` was fixed that
|
|
189
|
+
* way first and still threw `Invalid array length` on a NaN width, because it
|
|
190
|
+
* handed the unparsed original to `Measurement.get` and got a NaN column count
|
|
191
|
+
* back. Replace the field, and nothing downstream can see the number the
|
|
192
|
+
* caller actually wrote.
|
|
193
|
+
*
|
|
194
|
+
* [LAW:single-enforcer] This is the width checkpoint for the render contract.
|
|
195
|
+
* There is no second one: a renderable that re-derives its own answer is how
|
|
196
|
+
* `Panel`, `Table`, `Tree`, `Columns` and `Layout` came to disagree about what
|
|
197
|
+
* a NaN width means — one collapsed to a cell of garbage, two threw, one
|
|
198
|
+
* ignored the request and emitted its full natural width.
|
|
199
|
+
*/
|
|
200
|
+
export declare function withCellWidth(options: RenderOptions): RenderOptions;
|
|
201
|
+
/**
|
|
202
|
+
* The options a renderable should *lay out against*: `withCellWidth`, with an
|
|
203
|
+
* unbounded offer resolved to the renderable's own natural width.
|
|
204
|
+
*
|
|
205
|
+
* [LAW:parse-dont-validate] `cellCount` cannot finish the job alone. It floors
|
|
206
|
+
* a negative width and NaN to zero from the number alone, but `Infinity` is not
|
|
207
|
+
* a quantity it can floor — the only finite answer is "as wide as this
|
|
208
|
+
* renderable's content wants", which is a question about the renderable and not
|
|
209
|
+
* about the number. So the parse is completed here, where the renderable is in
|
|
210
|
+
* hand, and the stamped options travel exactly as `withCellWidth`'s do.
|
|
211
|
+
*
|
|
212
|
+
* [LAW:single-enforcer] One checkpoint for the whole rule, not one per
|
|
213
|
+
* renderable. `Panel`, `Padding`, `Columns` and `Layout` each expand into the
|
|
214
|
+
* width they are offered, and each independently reached `" ".repeat(Infinity)`
|
|
215
|
+
* — Columns twice over, since its column *count* is derived from the width too
|
|
216
|
+
* and `new Array(Infinity)` throws a different error again.
|
|
217
|
+
*
|
|
218
|
+
* The natural width comes from `measure`, which every one of them already
|
|
219
|
+
* implements, and the recursion terminates because `measure` reports content
|
|
220
|
+
* rather than the offer: `Panel.measure({maxWidth: Infinity})` is `{9, 9}` for
|
|
221
|
+
* nine cells of content and frame, not `{9, Infinity}`.
|
|
222
|
+
*
|
|
223
|
+
* So this belongs at the top of `render` and never at the top of `measure`:
|
|
224
|
+
* `measure` is the method being asked, and asking it through here would ask it
|
|
225
|
+
* with itself. `measure` parses with `withCellWidth` and reports a natural
|
|
226
|
+
* width of its own — that is the half of the contract that makes this half work.
|
|
227
|
+
*
|
|
228
|
+
* A `maximum` of `Infinity` means the renderable genuinely cannot answer. Two
|
|
229
|
+
* ways in: it wraps a `Renderable` with no `measure`, so nothing in the tree
|
|
230
|
+
* knows how wide the content wants to be; or something inside it asked for an
|
|
231
|
+
* unbounded width of its own, as a `Table` column declared
|
|
232
|
+
* `{ ratio: Infinity }` does. [LAW:no-silent-failure] Both are unanswerable
|
|
233
|
+
* rather than zero, and saying so names the cause — where `String.repeat` and
|
|
234
|
+
* `new Array` only ever name their own argument.
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* `options` as a measurement is asked with. A `measure` that renders to learn
|
|
238
|
+
* its shape draws nothing on the frame, so it reports no owner to `onDraw` —
|
|
239
|
+
* one that did would put a widget in document order at the moment a
|
|
240
|
+
* container measured it, and keep one the frame then cropped away.
|
|
241
|
+
*
|
|
242
|
+
* [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
|
|
243
|
+
* `withBoundedWidth` ask through it, and each `measure` passes it on.
|
|
244
|
+
*/
|
|
245
|
+
export declare function measuring(options: RenderOptions): RenderOptions;
|
|
246
|
+
export declare function withBoundedWidth(options: RenderOptions, self: Measurable): RenderOptions;
|
|
247
|
+
export interface Renderable {
|
|
248
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
249
|
+
}
|
|
250
|
+
export interface Measurable {
|
|
251
|
+
measure(options: RenderOptions): {
|
|
252
|
+
minimum: number;
|
|
253
|
+
maximum: number;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* What the wheel scrolls: an owner of anchors on the frame (`./anchor.ts`)
|
|
258
|
+
* that moves by lines. `canScrollBy` says whether `scrollBy` with the same
|
|
259
|
+
* `lines` would move it; one that would not passes the wheel outward.
|
|
260
|
+
*/
|
|
261
|
+
export interface Scrollable {
|
|
262
|
+
canScrollBy(lines: number): boolean;
|
|
263
|
+
scrollBy(lines: number): void;
|
|
264
|
+
}
|
|
265
|
+
export declare function isRenderable(obj: unknown): obj is Renderable;
|
|
266
|
+
export declare function isMeasurable(obj: unknown): obj is Measurable;
|
|
267
|
+
export declare function isScrollable(obj: object): obj is Scrollable;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering protocol interfaces — Renderable, Measurable, RenderOptions.
|
|
3
|
+
* [LAW:one-source-of-truth] These interfaces are the single authority for the rendering contract.
|
|
4
|
+
*/
|
|
5
|
+
import { cellCount } from "./cells.js";
|
|
6
|
+
import { DEFAULT_THEME } from "./style.js";
|
|
7
|
+
const ASCII_TEXT = /^[\x00-\x7F]*$/;
|
|
8
|
+
export function drawable(options, chosen, ascii, glyphs = String) {
|
|
9
|
+
return options.asciiOnly === true && !ASCII_TEXT.test(glyphs(chosen)) ? ascii : chosen;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The budget a renderable hands the one child filling its space: its own, less
|
|
13
|
+
* the `rows` it draws itself, and of the same kind.
|
|
14
|
+
*/
|
|
15
|
+
export function insetHeight(height, rows) {
|
|
16
|
+
return height && { rows: cellCount(height.rows - rows), exact: height.exact };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The budget a renderable hands each of several children it stacks: its rows,
|
|
20
|
+
* as a ceiling.
|
|
21
|
+
*/
|
|
22
|
+
export function stackedHeight(height) {
|
|
23
|
+
return height && { rows: cellCount(height.rows), exact: false };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The rows a region holds, parsed as a cell count, or `undefined` when there
|
|
27
|
+
* is no region to fill: a ceiling, no budget, or a region of `Infinity` rows,
|
|
28
|
+
* which names no count — as an unbounded width resolves to a natural one in
|
|
29
|
+
* `withBoundedWidth`.
|
|
30
|
+
*/
|
|
31
|
+
export function regionRows(height) {
|
|
32
|
+
return height?.exact && height.rows !== Infinity ? cellCount(height.rows) : undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What came back from a child, held to the region `height` names — blank rows
|
|
36
|
+
* padded below, overflow cropped from the bottom — or left at its own height
|
|
37
|
+
* when `height` is no region. This is the shaping a region's setter owes.
|
|
38
|
+
*/
|
|
39
|
+
export function fitHeight(lines, height) {
|
|
40
|
+
const rows = regionRows(height) ?? lines.length;
|
|
41
|
+
const fitted = lines.slice(0, rows);
|
|
42
|
+
while (fitted.length < rows)
|
|
43
|
+
fitted.push([]);
|
|
44
|
+
return fitted;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The style a `string | Style` stands for in this render: a name from the
|
|
48
|
+
* theme of whatever is drawing, otherwise a style definition. A string that is
|
|
49
|
+
* neither throws `StyleSyntaxError`. This is how a renderable that yields its
|
|
50
|
+
* own `Segment`s draws with a theme name, since a `Segment` takes a resolved
|
|
51
|
+
* `Style`. It is Rich's `console.get_style`, asked of the options because the
|
|
52
|
+
* options are where the theme travels.
|
|
53
|
+
*
|
|
54
|
+
* [LAW:single-enforcer] Every renderable resolves a style name here, at render
|
|
55
|
+
* time, because only the render knows whose theme it is drawing for. Resolved
|
|
56
|
+
* when a renderable was built instead, a name was fixed to the built-in
|
|
57
|
+
* defaults before any `Console` could offer its theme, and `new Console({
|
|
58
|
+
* theme })` changed nothing.
|
|
59
|
+
*/
|
|
60
|
+
export function getStyle(options, style) {
|
|
61
|
+
return (options.theme ?? DEFAULT_THEME).resolve(style);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The options a renderable should work from: the caller's, with `maxWidth`
|
|
65
|
+
* parsed into an actual count of cells.
|
|
66
|
+
*
|
|
67
|
+
* [LAW:parse-dont-validate] The stamp has to travel, which is the whole reason
|
|
68
|
+
* this returns options rather than a number. Parsing `options.maxWidth` into a
|
|
69
|
+
* local leaves the caller's raw value sitting in `options` for whatever the
|
|
70
|
+
* renderable forwards it to — and every renderable forwards it, to a child's
|
|
71
|
+
* `render`, to `Measurement.get`, to a nested layout. `Columns` was fixed that
|
|
72
|
+
* way first and still threw `Invalid array length` on a NaN width, because it
|
|
73
|
+
* handed the unparsed original to `Measurement.get` and got a NaN column count
|
|
74
|
+
* back. Replace the field, and nothing downstream can see the number the
|
|
75
|
+
* caller actually wrote.
|
|
76
|
+
*
|
|
77
|
+
* [LAW:single-enforcer] This is the width checkpoint for the render contract.
|
|
78
|
+
* There is no second one: a renderable that re-derives its own answer is how
|
|
79
|
+
* `Panel`, `Table`, `Tree`, `Columns` and `Layout` came to disagree about what
|
|
80
|
+
* a NaN width means — one collapsed to a cell of garbage, two threw, one
|
|
81
|
+
* ignored the request and emitted its full natural width.
|
|
82
|
+
*/
|
|
83
|
+
export function withCellWidth(options) {
|
|
84
|
+
return { ...options, maxWidth: cellCount(options.maxWidth) };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The options a renderable should *lay out against*: `withCellWidth`, with an
|
|
88
|
+
* unbounded offer resolved to the renderable's own natural width.
|
|
89
|
+
*
|
|
90
|
+
* [LAW:parse-dont-validate] `cellCount` cannot finish the job alone. It floors
|
|
91
|
+
* a negative width and NaN to zero from the number alone, but `Infinity` is not
|
|
92
|
+
* a quantity it can floor — the only finite answer is "as wide as this
|
|
93
|
+
* renderable's content wants", which is a question about the renderable and not
|
|
94
|
+
* about the number. So the parse is completed here, where the renderable is in
|
|
95
|
+
* hand, and the stamped options travel exactly as `withCellWidth`'s do.
|
|
96
|
+
*
|
|
97
|
+
* [LAW:single-enforcer] One checkpoint for the whole rule, not one per
|
|
98
|
+
* renderable. `Panel`, `Padding`, `Columns` and `Layout` each expand into the
|
|
99
|
+
* width they are offered, and each independently reached `" ".repeat(Infinity)`
|
|
100
|
+
* — Columns twice over, since its column *count* is derived from the width too
|
|
101
|
+
* and `new Array(Infinity)` throws a different error again.
|
|
102
|
+
*
|
|
103
|
+
* The natural width comes from `measure`, which every one of them already
|
|
104
|
+
* implements, and the recursion terminates because `measure` reports content
|
|
105
|
+
* rather than the offer: `Panel.measure({maxWidth: Infinity})` is `{9, 9}` for
|
|
106
|
+
* nine cells of content and frame, not `{9, Infinity}`.
|
|
107
|
+
*
|
|
108
|
+
* So this belongs at the top of `render` and never at the top of `measure`:
|
|
109
|
+
* `measure` is the method being asked, and asking it through here would ask it
|
|
110
|
+
* with itself. `measure` parses with `withCellWidth` and reports a natural
|
|
111
|
+
* width of its own — that is the half of the contract that makes this half work.
|
|
112
|
+
*
|
|
113
|
+
* A `maximum` of `Infinity` means the renderable genuinely cannot answer. Two
|
|
114
|
+
* ways in: it wraps a `Renderable` with no `measure`, so nothing in the tree
|
|
115
|
+
* knows how wide the content wants to be; or something inside it asked for an
|
|
116
|
+
* unbounded width of its own, as a `Table` column declared
|
|
117
|
+
* `{ ratio: Infinity }` does. [LAW:no-silent-failure] Both are unanswerable
|
|
118
|
+
* rather than zero, and saying so names the cause — where `String.repeat` and
|
|
119
|
+
* `new Array` only ever name their own argument.
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* `options` as a measurement is asked with. A `measure` that renders to learn
|
|
123
|
+
* its shape draws nothing on the frame, so it reports no owner to `onDraw` —
|
|
124
|
+
* one that did would put a widget in document order at the moment a
|
|
125
|
+
* container measured it, and keep one the frame then cropped away.
|
|
126
|
+
*
|
|
127
|
+
* [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
|
|
128
|
+
* `withBoundedWidth` ask through it, and each `measure` passes it on.
|
|
129
|
+
*/
|
|
130
|
+
export function measuring(options) {
|
|
131
|
+
return { ...options, onDraw: undefined };
|
|
132
|
+
}
|
|
133
|
+
export function withBoundedWidth(options, self) {
|
|
134
|
+
const parsed = withCellWidth(options);
|
|
135
|
+
if (Number.isFinite(parsed.maxWidth))
|
|
136
|
+
return parsed;
|
|
137
|
+
const natural = self.measure(measuring(parsed)).maximum;
|
|
138
|
+
if (!Number.isFinite(natural)) {
|
|
139
|
+
throw new RangeError("maxWidth is unbounded and this renderable has no natural width to fall " +
|
|
140
|
+
"back on: its content does not implement measure(), or something inside " +
|
|
141
|
+
"it asked for an unbounded width of its own. Render it at a finite " +
|
|
142
|
+
"width, or give the content a measure() that reports a finite maximum.");
|
|
143
|
+
}
|
|
144
|
+
return { ...parsed, maxWidth: cellCount(natural) };
|
|
145
|
+
}
|
|
146
|
+
export function isRenderable(obj) {
|
|
147
|
+
return (typeof obj === "object" &&
|
|
148
|
+
obj !== null &&
|
|
149
|
+
"render" in obj &&
|
|
150
|
+
typeof obj.render === "function");
|
|
151
|
+
}
|
|
152
|
+
export function isMeasurable(obj) {
|
|
153
|
+
return (typeof obj === "object" &&
|
|
154
|
+
obj !== null &&
|
|
155
|
+
"measure" in obj &&
|
|
156
|
+
typeof obj.measure === "function");
|
|
157
|
+
}
|
|
158
|
+
export function isScrollable(obj) {
|
|
159
|
+
return ("canScrollBy" in obj &&
|
|
160
|
+
typeof obj.canScrollBy === "function" &&
|
|
161
|
+
"scrollBy" in obj &&
|
|
162
|
+
typeof obj.scrollBy === "function");
|
|
163
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `renderToString` — stateless one-shot emission of a `Renderable` to a string
|
|
3
|
+
* of ANSI-encoded text. Pure function: same inputs produce byte-identical
|
|
4
|
+
* output. Does not write to `process.stdout` and does not require a `Console`
|
|
5
|
+
* instance.
|
|
6
|
+
*
|
|
7
|
+
* Note on `colorSystem: "auto"`: the `"auto"` spec resolves via
|
|
8
|
+
* `detectColorSystem`, which reads `process.env` and `process.stdout?.isTTY`
|
|
9
|
+
* by default. Callers that want a fully deterministic render must either pass
|
|
10
|
+
* an explicit `ColorDepth` enum / non-`"auto"` spec, or supply `env` and
|
|
11
|
+
* `isTTY` in the options so detection does not consult ambient process state.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:single-enforcer] The Segment-to-ANSI conversion lives in `segmentsToString`
|
|
14
|
+
* and is the single way segments become wire bytes. `Console._writeSegments`,
|
|
15
|
+
* `Live.refresh`, `Screen`'s frame paint, `renderToString`, and
|
|
16
|
+
* `segmentToString` all delegate here, so terminal output, live frames, widget
|
|
17
|
+
* frames, string export, and single-segment encoding agree by construction.
|
|
18
|
+
*
|
|
19
|
+
* [LAW:one-source-of-truth] What the encoding needs from where it writes
|
|
20
|
+
* arrives as one `Destination` value. Each caller resolves its destination
|
|
21
|
+
* once — spec, detection, and its own overrides — and hands it through whole,
|
|
22
|
+
* so a depth is never paired with a link setting taken from somewhere else.
|
|
23
|
+
*
|
|
24
|
+
* [LAW:dataflow-not-control-flow] The same pipeline runs every render: collect
|
|
25
|
+
* non-control non-empty pieces, partition by SGR-codes (SGR-runs), partition
|
|
26
|
+
* each run by link (link-runs), emit one SGR open/close per run with link
|
|
27
|
+
* open/close pairs sitting inside. Colour and hyperlinks are two facts about
|
|
28
|
+
* the destination: `colorSystem === null` empties every piece's SGR-codes, and
|
|
29
|
+
* `hyperlinks === false` empties every piece's link. A colour depth never
|
|
30
|
+
* removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
|
|
31
|
+
* follows OSC 8.
|
|
32
|
+
*
|
|
33
|
+
* [LAW:types-are-the-program] Adjacent same-style segments share an SGR wrap
|
|
34
|
+
* because the SGR-codes string is the same group key for both — the
|
|
35
|
+
* partitioning shape (data) encodes the byte structure; the emit walk is a
|
|
36
|
+
* mechanical fold over it.
|
|
37
|
+
*/
|
|
38
|
+
import { ColorDepth } from "./color.js";
|
|
39
|
+
import type { Destination } from "./color.js";
|
|
40
|
+
import type { Segment } from "./segment.js";
|
|
41
|
+
import type { Renderable, RenderOptions } from "./protocol.js";
|
|
42
|
+
export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "onStyleError"> {
|
|
43
|
+
/** Cell width to render into. Default 80. */
|
|
44
|
+
width?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Color encoding to emit. Accepts a string spec (`"auto"`, `"truecolor"`,
|
|
47
|
+
* `"256"`, `"ansi"`, `"none"`), a `ColorDepth` enum value, or `null` to
|
|
48
|
+
* strip SGR colors/attributes. Hyperlinks are `hyperlinks`' concern. Default
|
|
49
|
+
* truecolor.
|
|
50
|
+
*/
|
|
51
|
+
colorSystem?: string | ColorDepth | null;
|
|
52
|
+
/**
|
|
53
|
+
* Environment to consult when `colorSystem` is `"auto"`. Defaults to
|
|
54
|
+
* `process.env`. Pass an explicit value to keep rendering deterministic.
|
|
55
|
+
*/
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
/**
|
|
58
|
+
* Whether output is going to a TTY when `colorSystem` is `"auto"`. Defaults
|
|
59
|
+
* to `process.stdout?.isTTY`. Pass an explicit value to keep rendering
|
|
60
|
+
* deterministic.
|
|
61
|
+
*/
|
|
62
|
+
isTTY?: boolean;
|
|
63
|
+
/** When true, forces `colorSystem` to `null` regardless of the explicit value. */
|
|
64
|
+
noColor?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Whether OSC 8 hyperlinks are emitted. Independent of `colorSystem`: a
|
|
67
|
+
* null colour system strips SGR and keeps links. Default: what the
|
|
68
|
+
* destination takes — true for an explicit depth, detected under `"auto"`
|
|
69
|
+
* (no TTY or TERM=dumb: false). `false` with `colorSystem: null` is plain text.
|
|
70
|
+
*/
|
|
71
|
+
hyperlinks?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Encodes a single segment as ANSI bytes for `destination`. Equivalent to
|
|
75
|
+
* `segmentsToString([segment], destination)` — same SGR / OSC 8 layout.
|
|
76
|
+
*/
|
|
77
|
+
export declare function segmentToString(segment: Segment, destination: Destination): string;
|
|
78
|
+
/**
|
|
79
|
+
* Encodes a sequence of segments as ANSI bytes for `destination`, coalescing
|
|
80
|
+
* adjacent same-SGR segments under a single SGR open/close pair, with OSC 8
|
|
81
|
+
* link pairs nested inside per same-link sub-run.
|
|
82
|
+
*/
|
|
83
|
+
export declare function segmentsToString(segments: Iterable<Segment>, destination: Destination): string;
|
|
84
|
+
export declare function renderToString(renderable: Renderable, options?: RenderToStringOptions): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `renderToString` — stateless one-shot emission of a `Renderable` to a string
|
|
3
|
+
* of ANSI-encoded text. Pure function: same inputs produce byte-identical
|
|
4
|
+
* output. Does not write to `process.stdout` and does not require a `Console`
|
|
5
|
+
* instance.
|
|
6
|
+
*
|
|
7
|
+
* Note on `colorSystem: "auto"`: the `"auto"` spec resolves via
|
|
8
|
+
* `detectColorSystem`, which reads `process.env` and `process.stdout?.isTTY`
|
|
9
|
+
* by default. Callers that want a fully deterministic render must either pass
|
|
10
|
+
* an explicit `ColorDepth` enum / non-`"auto"` spec, or supply `env` and
|
|
11
|
+
* `isTTY` in the options so detection does not consult ambient process state.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:single-enforcer] The Segment-to-ANSI conversion lives in `segmentsToString`
|
|
14
|
+
* and is the single way segments become wire bytes. `Console._writeSegments`,
|
|
15
|
+
* `Live.refresh`, `Screen`'s frame paint, `renderToString`, and
|
|
16
|
+
* `segmentToString` all delegate here, so terminal output, live frames, widget
|
|
17
|
+
* frames, string export, and single-segment encoding agree by construction.
|
|
18
|
+
*
|
|
19
|
+
* [LAW:one-source-of-truth] What the encoding needs from where it writes
|
|
20
|
+
* arrives as one `Destination` value. Each caller resolves its destination
|
|
21
|
+
* once — spec, detection, and its own overrides — and hands it through whole,
|
|
22
|
+
* so a depth is never paired with a link setting taken from somewhere else.
|
|
23
|
+
*
|
|
24
|
+
* [LAW:dataflow-not-control-flow] The same pipeline runs every render: collect
|
|
25
|
+
* non-control non-empty pieces, partition by SGR-codes (SGR-runs), partition
|
|
26
|
+
* each run by link (link-runs), emit one SGR open/close per run with link
|
|
27
|
+
* open/close pairs sitting inside. Colour and hyperlinks are two facts about
|
|
28
|
+
* the destination: `colorSystem === null` empties every piece's SGR-codes, and
|
|
29
|
+
* `hyperlinks === false` empties every piece's link. A colour depth never
|
|
30
|
+
* removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
|
|
31
|
+
* follows OSC 8.
|
|
32
|
+
*
|
|
33
|
+
* [LAW:types-are-the-program] Adjacent same-style segments share an SGR wrap
|
|
34
|
+
* because the SGR-codes string is the same group key for both — the
|
|
35
|
+
* partitioning shape (data) encodes the byte structure; the emit walk is a
|
|
36
|
+
* mechanical fold over it.
|
|
37
|
+
*/
|
|
38
|
+
import { ColorDepth, resolveDestination } from "./color.js";
|
|
39
|
+
import { OSC8_CLOSE, osc8Open } from "./osc8.js";
|
|
40
|
+
const DEFAULT_WIDTH = 80;
|
|
41
|
+
function segmentToPiece(segment, destination) {
|
|
42
|
+
if (segment.isControl)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (segment.text.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
const style = segment.style;
|
|
47
|
+
if (!style || style.isNull) {
|
|
48
|
+
return { text: segment.text, sgrCodes: "", link: undefined };
|
|
49
|
+
}
|
|
50
|
+
// [LAW:one-type-per-behavior] Colour depth governs SGR only; hyperlinks are
|
|
51
|
+
// their own fact. Coupling them let a colour setting delete every control.
|
|
52
|
+
return {
|
|
53
|
+
text: segment.text,
|
|
54
|
+
sgrCodes: destination.colorSystem === null ? "" : style.toSgrCodes(destination.colorSystem),
|
|
55
|
+
link: destination.hyperlinks ? style.link : undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Encodes a single segment as ANSI bytes for `destination`. Equivalent to
|
|
60
|
+
* `segmentsToString([segment], destination)` — same SGR / OSC 8 layout.
|
|
61
|
+
*/
|
|
62
|
+
export function segmentToString(segment, destination) {
|
|
63
|
+
return segmentsToString([segment], destination);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Encodes a sequence of segments as ANSI bytes for `destination`, coalescing
|
|
67
|
+
* adjacent same-SGR segments under a single SGR open/close pair, with OSC 8
|
|
68
|
+
* link pairs nested inside per same-link sub-run.
|
|
69
|
+
*/
|
|
70
|
+
export function segmentsToString(segments, destination) {
|
|
71
|
+
const pieces = [];
|
|
72
|
+
for (const s of segments) {
|
|
73
|
+
const p = segmentToPiece(s, destination);
|
|
74
|
+
if (p)
|
|
75
|
+
pieces.push(p);
|
|
76
|
+
}
|
|
77
|
+
if (pieces.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
// [LAW:dataflow-not-control-flow] One linear chunk accumulator for the
|
|
80
|
+
// entire output; SGR / OSC 8 boundaries and piece texts all push into it
|
|
81
|
+
// in order. No per-link-run intermediate string, no quadratic `+=` chains.
|
|
82
|
+
const parts = [];
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < pieces.length) {
|
|
85
|
+
const sgr = pieces[i].sgrCodes;
|
|
86
|
+
let j = i + 1;
|
|
87
|
+
while (j < pieces.length && pieces[j].sgrCodes === sgr)
|
|
88
|
+
j++;
|
|
89
|
+
if (sgr.length > 0)
|
|
90
|
+
parts.push(`\x1b[${sgr}m`);
|
|
91
|
+
let k = i;
|
|
92
|
+
while (k < j) {
|
|
93
|
+
const link = pieces[k].link;
|
|
94
|
+
let l = k + 1;
|
|
95
|
+
while (l < j && pieces[l].link === link)
|
|
96
|
+
l++;
|
|
97
|
+
if (link) {
|
|
98
|
+
parts.push(osc8Open(link));
|
|
99
|
+
for (let m = k; m < l; m++)
|
|
100
|
+
parts.push(pieces[m].text);
|
|
101
|
+
parts.push(OSC8_CLOSE);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
for (let m = k; m < l; m++)
|
|
105
|
+
parts.push(pieces[m].text);
|
|
106
|
+
}
|
|
107
|
+
k = l;
|
|
108
|
+
}
|
|
109
|
+
if (sgr.length > 0)
|
|
110
|
+
parts.push("\x1b[0m");
|
|
111
|
+
i = j;
|
|
112
|
+
}
|
|
113
|
+
return parts.join("");
|
|
114
|
+
}
|
|
115
|
+
export function renderToString(renderable, options) {
|
|
116
|
+
const width = options?.width ?? DEFAULT_WIDTH;
|
|
117
|
+
// [LAW:dataflow-not-control-flow] An absent spec is truecolor; an explicit
|
|
118
|
+
// `null` stays null (`??` would collapse it).
|
|
119
|
+
// [LAW:single-enforcer] Specs resolve through `resolveDestination`.
|
|
120
|
+
const detectOptions = {};
|
|
121
|
+
if (options?.env !== undefined)
|
|
122
|
+
detectOptions.env = options.env;
|
|
123
|
+
if (options?.isTTY !== undefined)
|
|
124
|
+
detectOptions.isTTY = options.isTTY;
|
|
125
|
+
const rawSpec = options?.colorSystem;
|
|
126
|
+
const resolved = resolveDestination(rawSpec === undefined ? ColorDepth.TRUECOLOR : rawSpec, detectOptions);
|
|
127
|
+
// [LAW:single-enforcer] This call's overrides are applied here, once, to
|
|
128
|
+
// make the one destination everything below is drawn for. `noColor` is a
|
|
129
|
+
// colour choice, so it overrides the depth and leaves hyperlinks to the
|
|
130
|
+
// destination; an explicit `hyperlinks` overrides what the destination takes.
|
|
131
|
+
const destination = {
|
|
132
|
+
colorSystem: options?.noColor ? null : resolved.colorSystem,
|
|
133
|
+
hyperlinks: options?.hyperlinks ?? resolved.hyperlinks,
|
|
134
|
+
};
|
|
135
|
+
const renderOptions = {
|
|
136
|
+
maxWidth: width,
|
|
137
|
+
isTerminal: false,
|
|
138
|
+
asciiOnly: options?.asciiOnly ?? false,
|
|
139
|
+
onStyleError: options?.onStyleError,
|
|
140
|
+
// [LAW:one-source-of-truth] The depth the segments below are encoded at,
|
|
141
|
+
// so a renderable measures what this very call will draw.
|
|
142
|
+
colorSystem: destination.colorSystem,
|
|
143
|
+
};
|
|
144
|
+
return segmentsToString(renderable.render(renderOptions), destination);
|
|
145
|
+
}
|