@textui/core 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/app/app.d.ts +200 -0
- package/dist/app/app.d.ts.map +1 -0
- package/dist/app/app.js +980 -0
- package/dist/core/animation.d.ts +34 -0
- package/dist/core/animation.d.ts.map +1 -0
- package/dist/core/animation.js +125 -0
- package/dist/core/clipboard.d.ts +29 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/clipboard.js +30 -0
- package/dist/core/commands.d.ts +42 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +146 -0
- package/dist/core/components.d.ts +27 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +78 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +77 -0
- package/dist/core/focus.d.ts +69 -0
- package/dist/core/focus.d.ts.map +1 -0
- package/dist/core/focus.js +336 -0
- package/dist/core/i18n.d.ts +27 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +88 -0
- package/dist/core/keybindings.d.ts +53 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +163 -0
- package/dist/core/layers.d.ts +28 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +84 -0
- package/dist/core/manifest.d.ts +22 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +85 -0
- package/dist/core/navigation.d.ts +47 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +110 -0
- package/dist/core/resources.d.ts +115 -0
- package/dist/core/resources.d.ts.map +1 -0
- package/dist/core/resources.js +321 -0
- package/dist/core/services.d.ts +21 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +63 -0
- package/dist/core/store.d.ts +60 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +593 -0
- package/dist/core/surfaces.d.ts +81 -0
- package/dist/core/surfaces.d.ts.map +1 -0
- package/dist/core/surfaces.js +237 -0
- package/dist/core/syntax.d.ts +49 -0
- package/dist/core/syntax.d.ts.map +1 -0
- package/dist/core/syntax.js +172 -0
- package/dist/core/when.d.ts +15 -0
- package/dist/core/when.d.ts.map +1 -0
- package/dist/core/when.js +224 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/jsx/factory.d.ts +27 -0
- package/dist/jsx/factory.d.ts.map +1 -0
- package/dist/jsx/factory.js +102 -0
- package/dist/jsx/intrinsics.d.ts +96 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -0
- package/dist/jsx/intrinsics.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime.js +12 -0
- package/dist/jsx/jsx-runtime.d.ts +42 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +16 -0
- package/dist/render/buffer.d.ts +77 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +275 -0
- package/dist/render/color.d.ts +35 -0
- package/dist/render/color.d.ts.map +1 -0
- package/dist/render/color.js +160 -0
- package/dist/render/diff.d.ts +37 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +66 -0
- package/dist/render/layout.d.ts +72 -0
- package/dist/render/layout.d.ts.map +1 -0
- package/dist/render/layout.js +633 -0
- package/dist/render/static.d.ts +63 -0
- package/dist/render/static.d.ts.map +1 -0
- package/dist/render/static.js +209 -0
- package/dist/runtime/bindings.d.ts +46 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +116 -0
- package/dist/runtime/hooks.d.ts +284 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +846 -0
- package/dist/runtime/instance.d.ts +106 -0
- package/dist/runtime/instance.d.ts.map +1 -0
- package/dist/runtime/instance.js +181 -0
- package/dist/runtime/paint.d.ts +27 -0
- package/dist/runtime/paint.d.ts.map +1 -0
- package/dist/runtime/paint.js +567 -0
- package/dist/runtime/reconcile.d.ts +23 -0
- package/dist/runtime/reconcile.d.ts.map +1 -0
- package/dist/runtime/reconcile.js +260 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/style.d.ts +55 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/style.js +143 -0
- package/dist/themes/borders.d.ts +10 -0
- package/dist/themes/borders.d.ts.map +1 -0
- package/dist/themes/borders.js +88 -0
- package/dist/themes/builtin.d.ts +31 -0
- package/dist/themes/builtin.d.ts.map +1 -0
- package/dist/themes/builtin.js +291 -0
- package/dist/themes/glyphs.d.ts +15 -0
- package/dist/themes/glyphs.d.ts.map +1 -0
- package/dist/themes/glyphs.js +97 -0
- package/dist/themes/index.d.ts +5 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/registry.d.ts +20 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +204 -0
- package/dist/types/adapter.d.ts +38 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/animation.d.ts +31 -0
- package/dist/types/animation.d.ts.map +1 -0
- package/dist/types/animation.js +1 -0
- package/dist/types/app.d.ts +148 -0
- package/dist/types/app.d.ts.map +1 -0
- package/dist/types/app.js +1 -0
- package/dist/types/async.d.ts +30 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/async.js +1 -0
- package/dist/types/capabilities.d.ts +44 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +33 -0
- package/dist/types/cells.d.ts +72 -0
- package/dist/types/cells.d.ts.map +1 -0
- package/dist/types/cells.js +10 -0
- package/dist/types/command.d.ts +127 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +1 -0
- package/dist/types/component-registry.d.ts +77 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/component-registry.js +1 -0
- package/dist/types/disposable.d.ts +8 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/focus.d.ts +72 -0
- package/dist/types/focus.d.ts.map +1 -0
- package/dist/types/focus.js +1 -0
- package/dist/types/geometry.d.ts +28 -0
- package/dist/types/geometry.d.ts.map +1 -0
- package/dist/types/geometry.js +14 -0
- package/dist/types/graph.d.ts +129 -0
- package/dist/types/graph.d.ts.map +1 -0
- package/dist/types/graph.js +31 -0
- package/dist/types/i18n.d.ts +23 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +31 -0
- package/dist/types/input.d.ts +65 -0
- package/dist/types/input.d.ts.map +1 -0
- package/dist/types/input.js +1 -0
- package/dist/types/keybinding.d.ts +49 -0
- package/dist/types/keybinding.d.ts.map +1 -0
- package/dist/types/keybinding.js +1 -0
- package/dist/types/layer.d.ts +54 -0
- package/dist/types/layer.d.ts.map +1 -0
- package/dist/types/layer.js +1 -0
- package/dist/types/manifest.d.ts +114 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/markdown.d.ts +58 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/markdown.js +1 -0
- package/dist/types/navigation.d.ts +40 -0
- package/dist/types/navigation.d.ts.map +1 -0
- package/dist/types/navigation.js +1 -0
- package/dist/types/render.d.ts +72 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +1 -0
- package/dist/types/resource.d.ts +170 -0
- package/dist/types/resource.d.ts.map +1 -0
- package/dist/types/resource.js +1 -0
- package/dist/types/services.d.ts +24 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +3 -0
- package/dist/types/shell.d.ts +32 -0
- package/dist/types/shell.d.ts.map +1 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/store.d.ts +124 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/store.js +1 -0
- package/dist/types/stream.d.ts +18 -0
- package/dist/types/stream.d.ts.map +1 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/style.d.ts +166 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/style.js +1 -0
- package/dist/types/surface.d.ts +97 -0
- package/dist/types/surface.d.ts.map +1 -0
- package/dist/types/surface.js +1 -0
- package/dist/types/syntax.d.ts +68 -0
- package/dist/types/syntax.d.ts.map +1 -0
- package/dist/types/syntax.js +5 -0
- package/dist/types/terminal.d.ts +52 -0
- package/dist/types/terminal.d.ts.map +1 -0
- package/dist/types/terminal.js +1 -0
- package/dist/types/theme.d.ts +117 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/when.d.ts +19 -0
- package/dist/types/when.d.ts.map +1 -0
- package/dist/types/when.js +1 -0
- package/dist/ui/primitives.d.ts +56 -0
- package/dist/ui/primitives.d.ts.map +1 -0
- package/dist/ui/primitives.js +108 -0
- package/dist/ui/screen.d.ts +23 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +18 -0
- package/dist/util/disposable.d.ts +6 -0
- package/dist/util/disposable.d.ts.map +1 -0
- package/dist/util/disposable.js +49 -0
- package/dist/util/markdown.d.ts +24 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +220 -0
- package/dist/util/paths.d.ts +47 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +134 -0
- package/dist/util/stream.d.ts +32 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +206 -0
- package/dist/util/text.d.ts +65 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +419 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +11 -0
- package/src/app/app.ts +1096 -0
- package/src/core/animation.ts +144 -0
- package/src/core/clipboard.ts +40 -0
- package/src/core/commands.ts +169 -0
- package/src/core/components.ts +91 -0
- package/src/core/events.ts +95 -0
- package/src/core/focus.ts +358 -0
- package/src/core/i18n.ts +107 -0
- package/src/core/keybindings.ts +184 -0
- package/src/core/layers.ts +94 -0
- package/src/core/manifest.ts +84 -0
- package/src/core/navigation.ts +135 -0
- package/src/core/resources.ts +362 -0
- package/src/core/services.ts +69 -0
- package/src/core/store.ts +640 -0
- package/src/core/surfaces.ts +292 -0
- package/src/core/syntax.ts +200 -0
- package/src/core/when.ts +238 -0
- package/src/index.ts +76 -0
- package/src/jsx/factory.ts +124 -0
- package/src/jsx/intrinsics.ts +99 -0
- package/src/jsx/jsx-dev-runtime.ts +23 -0
- package/src/jsx/jsx-runtime.ts +76 -0
- package/src/render/buffer.ts +318 -0
- package/src/render/color.ts +180 -0
- package/src/render/diff.ts +99 -0
- package/src/render/layout.ts +764 -0
- package/src/render/static.ts +290 -0
- package/src/runtime/bindings.ts +150 -0
- package/src/runtime/hooks.ts +1057 -0
- package/src/runtime/instance.ts +284 -0
- package/src/runtime/paint.ts +737 -0
- package/src/runtime/reconcile.ts +335 -0
- package/src/runtime/runtime.ts +47 -0
- package/src/runtime/style.ts +205 -0
- package/src/themes/borders.ts +95 -0
- package/src/themes/builtin.ts +301 -0
- package/src/themes/glyphs.ts +101 -0
- package/src/themes/index.ts +4 -0
- package/src/themes/registry.ts +224 -0
- package/src/types/adapter.ts +41 -0
- package/src/types/animation.ts +34 -0
- package/src/types/app.ts +149 -0
- package/src/types/async.ts +35 -0
- package/src/types/capabilities.ts +79 -0
- package/src/types/cells.ts +79 -0
- package/src/types/command.ts +139 -0
- package/src/types/component-registry.ts +82 -0
- package/src/types/disposable.ts +8 -0
- package/src/types/focus.ts +77 -0
- package/src/types/geometry.ts +50 -0
- package/src/types/graph.ts +174 -0
- package/src/types/i18n.ts +25 -0
- package/src/types/index.ts +31 -0
- package/src/types/input.ts +81 -0
- package/src/types/keybinding.ts +51 -0
- package/src/types/layer.ts +46 -0
- package/src/types/manifest.ts +101 -0
- package/src/types/markdown.ts +47 -0
- package/src/types/navigation.ts +42 -0
- package/src/types/render.ts +86 -0
- package/src/types/resource.ts +185 -0
- package/src/types/services.ts +28 -0
- package/src/types/shell.ts +30 -0
- package/src/types/store.ts +148 -0
- package/src/types/stream.ts +24 -0
- package/src/types/style.ts +205 -0
- package/src/types/surface.ts +119 -0
- package/src/types/syntax.ts +93 -0
- package/src/types/terminal.ts +58 -0
- package/src/types/theme.ts +121 -0
- package/src/types/when.ts +21 -0
- package/src/ui/primitives.ts +118 -0
- package/src/ui/screen.ts +41 -0
- package/src/util/disposable.ts +49 -0
- package/src/util/markdown.ts +225 -0
- package/src/util/paths.ts +138 -0
- package/src/util/stream.ts +213 -0
- package/src/util/text.ts +428 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { ComponentNode } from '../types/graph.js';
|
|
2
|
+
import type { ComponentDefinition } from '../types/component-registry.js';
|
|
3
|
+
import type { ThemeDefinition } from '../types/theme.js';
|
|
4
|
+
import type { TerminalCapabilities, CapabilityOverrides } from '../types/capabilities.js';
|
|
5
|
+
import type { Instance } from '../runtime/instance.js';
|
|
6
|
+
import type { Runtime } from '../runtime/runtime.js';
|
|
7
|
+
import type { LayoutBox } from './layout.js';
|
|
8
|
+
import { Buffer } from './buffer.js';
|
|
9
|
+
import { layout, resolveEdges } from './layout.js';
|
|
10
|
+
import { buildBoxes, paintTree, type PaintEnv } from '../runtime/paint.js';
|
|
11
|
+
import { collectEffects, disposeTree, renderTree } from '../runtime/reconcile.js';
|
|
12
|
+
import { flushMeasures } from '../runtime/hooks.js';
|
|
13
|
+
import { NO_INTERACTION } from '../runtime/style.js';
|
|
14
|
+
import { createStore } from '../core/store.js';
|
|
15
|
+
import { createEvents } from '../core/events.js';
|
|
16
|
+
import { createWhen } from '../core/when.js';
|
|
17
|
+
import { createComponents } from '../core/components.js';
|
|
18
|
+
import { createServices } from '../core/services.js';
|
|
19
|
+
import { createFocus } from '../core/focus.js';
|
|
20
|
+
import { createLayers } from '../core/layers.js';
|
|
21
|
+
import { createAnimation } from '../core/animation.js';
|
|
22
|
+
import { createI18n } from '../core/i18n.js';
|
|
23
|
+
import { createThemes } from '../themes/registry.js';
|
|
24
|
+
import { PRIMITIVES } from '../ui/primitives.js';
|
|
25
|
+
import { FULL_CAPABILITIES } from '../types/capabilities.js';
|
|
26
|
+
import { ZERO_EDGES } from '../types/geometry.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One-shot rendering.
|
|
30
|
+
*
|
|
31
|
+
* The same component model, no terminal and no frame loop - which is what
|
|
32
|
+
* makes it useful for reports, `--help` output, generated documentation and
|
|
33
|
+
* tests. A component that only works interactively is a component that cannot
|
|
34
|
+
* be tested cheaply, so this path is not an afterthought.
|
|
35
|
+
*
|
|
36
|
+
* `renderOnce`, not `render`, because the difference that matters is lifetime.
|
|
37
|
+
* This draws a frame and hands it back; `render` mounts an application onto a
|
|
38
|
+
* terminal and keeps running. Two functions called `render` where one returns
|
|
39
|
+
* a string and the other takes over your screen is a trap, not a convenience -
|
|
40
|
+
* and every other library's `render` is the second one.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export interface StaticRenderOptions {
|
|
44
|
+
width?: number;
|
|
45
|
+
height?: number | 'auto';
|
|
46
|
+
theme?: string;
|
|
47
|
+
themes?: ThemeDefinition[];
|
|
48
|
+
/**
|
|
49
|
+
* Components to register by name.
|
|
50
|
+
*
|
|
51
|
+
* Only nodes addressed by name need this. A screen written in JSX carries
|
|
52
|
+
* its components with it - `<Card/>` compiles to a node holding the
|
|
53
|
+
* imported function, and the runtime uses that in preference to any
|
|
54
|
+
* registry - so this is for screens authored as data: JSON, a template
|
|
55
|
+
* renderer, anything naming a component in a string.
|
|
56
|
+
*
|
|
57
|
+
* Pass `CATALOG` for the shipped set.
|
|
58
|
+
*/
|
|
59
|
+
components?: ComponentDefinition[];
|
|
60
|
+
capabilities?: CapabilityOverrides;
|
|
61
|
+
locale?: string;
|
|
62
|
+
/** Seed the store before rendering. */
|
|
63
|
+
initialState?: Record<string, unknown>;
|
|
64
|
+
/** Passes allowed for effects that change state. */
|
|
65
|
+
maxPasses?: number;
|
|
66
|
+
onError?(err: unknown, context: string): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface StaticRenderResult {
|
|
70
|
+
buffer: Buffer;
|
|
71
|
+
text: string;
|
|
72
|
+
root: Instance;
|
|
73
|
+
runtime: Runtime;
|
|
74
|
+
/** Release subscriptions and timers. Always call this. */
|
|
75
|
+
dispose(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DEFAULT_CAPABILITIES: TerminalCapabilities = {
|
|
79
|
+
...FULL_CAPABILITIES,
|
|
80
|
+
mouse: false,
|
|
81
|
+
wheel: false,
|
|
82
|
+
focusEvents: false,
|
|
83
|
+
paste: false,
|
|
84
|
+
altScreen: false,
|
|
85
|
+
synchronizedOutput: false,
|
|
86
|
+
kittyKeyboard: false,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function createStaticRuntime(options: StaticRenderOptions = {}): {
|
|
90
|
+
runtime: Runtime;
|
|
91
|
+
setSize(width: number, height: number): void;
|
|
92
|
+
dispose(): void;
|
|
93
|
+
} {
|
|
94
|
+
const store = createStore();
|
|
95
|
+
const events = createEvents();
|
|
96
|
+
const when = createWhen(store);
|
|
97
|
+
const components = createComponents();
|
|
98
|
+
const services = createServices();
|
|
99
|
+
const focus = createFocus();
|
|
100
|
+
const layers = createLayers();
|
|
101
|
+
const animation = createAnimation({ manual: true, enabled: false });
|
|
102
|
+
const i18n = createI18n(options.locale ?? 'en');
|
|
103
|
+
const themes = createThemes(options.themes);
|
|
104
|
+
|
|
105
|
+
// The four primitives, and nothing else by default.
|
|
106
|
+
//
|
|
107
|
+
// This registered the whole catalog until it didn't: some eighty entries a
|
|
108
|
+
// screen made of `box` and `text` never looked at, and that JSX never
|
|
109
|
+
// needed either, since an imported component travels on its own node.
|
|
110
|
+
// Naming a component in data is the case a registry is for, and that case
|
|
111
|
+
// asks for it through `components`.
|
|
112
|
+
components.registerMany(PRIMITIVES);
|
|
113
|
+
if (options.components) components.registerMany(options.components);
|
|
114
|
+
|
|
115
|
+
const capabilities: TerminalCapabilities = {
|
|
116
|
+
...DEFAULT_CAPABILITIES,
|
|
117
|
+
...options.capabilities,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let size = { width: options.width ?? 80, height: 24 };
|
|
121
|
+
|
|
122
|
+
const onError = options.onError ?? ((err: unknown, context: string) => {
|
|
123
|
+
console.error(`[textui] ${context}`, err);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
store.onError = onError;
|
|
127
|
+
events.onError = onError;
|
|
128
|
+
|
|
129
|
+
if (options.initialState) {
|
|
130
|
+
store.batch(() => {
|
|
131
|
+
for (const [path, value] of Object.entries(options.initialState as object)) {
|
|
132
|
+
store.set((path.startsWith('$/') ? path : `$/${path}`) as `$/${string}`, value);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
store.set('$/modus/capabilities', capabilities);
|
|
138
|
+
store.set('$/modus/size', size);
|
|
139
|
+
|
|
140
|
+
const runtime: Runtime = {
|
|
141
|
+
store, events, when, components, services, focus, layers, animation, i18n,
|
|
142
|
+
theme: () => themes.resolve(options.theme ?? 'dark', capabilities),
|
|
143
|
+
capabilities: () => capabilities,
|
|
144
|
+
size: () => size,
|
|
145
|
+
execute: (id) => {
|
|
146
|
+
throw new Error(`[textui] command "${id}" cannot run under the static renderer`);
|
|
147
|
+
},
|
|
148
|
+
emit: (path, payload) => events.emit(path as `@/${string}`, payload),
|
|
149
|
+
requestRender: () => {},
|
|
150
|
+
app: () => null,
|
|
151
|
+
onError,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
runtime,
|
|
156
|
+
setSize(width, height) {
|
|
157
|
+
size = { width, height };
|
|
158
|
+
store.set('$/modus/size', size);
|
|
159
|
+
},
|
|
160
|
+
dispose() {
|
|
161
|
+
animation.dispose();
|
|
162
|
+
layers.dispose();
|
|
163
|
+
events.dispose();
|
|
164
|
+
store.dispose();
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function rootBoxFor(children: LayoutBox[]): LayoutBox {
|
|
170
|
+
return {
|
|
171
|
+
style: { direction: 'column' },
|
|
172
|
+
borderEdges: ZERO_EDGES,
|
|
173
|
+
children,
|
|
174
|
+
rect: { x: 0, y: 0, width: 0, height: 0 },
|
|
175
|
+
content: { x: 0, y: 0, width: 0, height: 0 },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderOnce(
|
|
180
|
+
node: ComponentNode,
|
|
181
|
+
options: StaticRenderOptions = {},
|
|
182
|
+
): StaticRenderResult {
|
|
183
|
+
const width = options.width ?? 80;
|
|
184
|
+
const { runtime, setSize, dispose: disposeRuntime } = createStaticRuntime(options);
|
|
185
|
+
|
|
186
|
+
// Measure against a generous height first when the caller wants `auto`, so
|
|
187
|
+
// nothing is clipped before we know how tall the content actually is.
|
|
188
|
+
const measureHeight = options.height === 'auto' || options.height === undefined
|
|
189
|
+
? 4096
|
|
190
|
+
: options.height;
|
|
191
|
+
setSize(width, measureHeight);
|
|
192
|
+
|
|
193
|
+
const env: PaintEnv = {
|
|
194
|
+
theme: runtime.theme(),
|
|
195
|
+
capabilities: runtime.capabilities(),
|
|
196
|
+
stateOf: () => NO_INTERACTION,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let root: Instance | null = null;
|
|
200
|
+
const maxPasses = options.maxPasses ?? 8;
|
|
201
|
+
|
|
202
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
203
|
+
root = renderTree(runtime, root, node);
|
|
204
|
+
const effects = collectEffects(root);
|
|
205
|
+
if (effects.length === 0) break;
|
|
206
|
+
for (const effect of effects) effect();
|
|
207
|
+
if (!hasDirty(root)) break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let instance = root as Instance;
|
|
211
|
+
let boxes = buildBoxes(instance, env);
|
|
212
|
+
let rootBox = rootBoxFor(boxes);
|
|
213
|
+
layout(rootBox, { x: 0, y: 0, width, height: measureHeight });
|
|
214
|
+
|
|
215
|
+
// A measured component only knows its size once it has been laid out. Give
|
|
216
|
+
// it that, then render again - otherwise the static renderer is the one
|
|
217
|
+
// place where a viewport draws nothing.
|
|
218
|
+
for (let pass = 0; pass < maxPasses && flushMeasures(); pass++) {
|
|
219
|
+
instance = renderTree(runtime, instance, node);
|
|
220
|
+
for (const effect of collectEffects(instance)) effect();
|
|
221
|
+
boxes = buildBoxes(instance, env);
|
|
222
|
+
rootBox = rootBoxFor(boxes);
|
|
223
|
+
layout(rootBox, { x: 0, y: 0, width, height: measureHeight });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const used = contentHeight(rootBox);
|
|
227
|
+
const height = options.height === 'auto' || options.height === undefined
|
|
228
|
+
? Math.max(1, used)
|
|
229
|
+
: options.height;
|
|
230
|
+
|
|
231
|
+
// Lay out again at the real height so `flex` and `justify` see the truth.
|
|
232
|
+
if (height !== measureHeight) {
|
|
233
|
+
setSize(width, height);
|
|
234
|
+
layout(rootBox, { x: 0, y: 0, width, height });
|
|
235
|
+
|
|
236
|
+
for (let pass = 0; pass < maxPasses && flushMeasures(); pass++) {
|
|
237
|
+
instance = renderTree(runtime, instance, node);
|
|
238
|
+
for (const effect of collectEffects(instance)) effect();
|
|
239
|
+
rootBox = rootBoxFor(buildBoxes(instance, env));
|
|
240
|
+
layout(rootBox, { x: 0, y: 0, width, height });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const buffer = new Buffer(width, height);
|
|
245
|
+
paintTree(buffer, instance, env, { x: 0, y: 0, width, height });
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
buffer,
|
|
249
|
+
text: buffer.toText(),
|
|
250
|
+
root: instance,
|
|
251
|
+
runtime,
|
|
252
|
+
dispose() {
|
|
253
|
+
disposeTree(instance);
|
|
254
|
+
disposeRuntime();
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Render to plain text. The shortest path from a component to a string. */
|
|
260
|
+
export function renderToString(node: ComponentNode, options: StaticRenderOptions = {}): string {
|
|
261
|
+
const result = renderOnce(node, options);
|
|
262
|
+
try {
|
|
263
|
+
return result.text;
|
|
264
|
+
} finally {
|
|
265
|
+
result.dispose();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function contentHeight(box: LayoutBox): number {
|
|
270
|
+
let bottom = 0;
|
|
271
|
+
const walk = (b: LayoutBox): void => {
|
|
272
|
+
if (b.rect.width > 0 && b.rect.height > 0) {
|
|
273
|
+
// Plus the margin below it, which is space the box needs even though
|
|
274
|
+
// nothing paints there. Measuring to the bottom edge alone reports a
|
|
275
|
+
// height the layout cannot reproduce: laid out again inside it, the
|
|
276
|
+
// margin has to come out of the box, and the box loses the rows.
|
|
277
|
+
// `<box margin={2} border="single">` measured 5 and then drew one row
|
|
278
|
+
// of a three-row box.
|
|
279
|
+
bottom = Math.max(bottom, b.rect.y + b.rect.height + resolveEdges(b.style.margin).bottom);
|
|
280
|
+
}
|
|
281
|
+
for (const child of b.children) walk(child);
|
|
282
|
+
};
|
|
283
|
+
for (const child of box.children) walk(child);
|
|
284
|
+
return bottom;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasDirty(instance: Instance): boolean {
|
|
288
|
+
if (instance.dirty) return true;
|
|
289
|
+
return instance.children.some(hasDirty);
|
|
290
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Action, BindingPath, ComponentNode, DataBinding, FunctionCall,
|
|
3
|
+
} from '../types/graph.js';
|
|
4
|
+
import type { ReactiveStore } from '../types/store.js';
|
|
5
|
+
import { isAction, isComponentNode, isDataBinding, isFunctionCall } from '../types/graph.js';
|
|
6
|
+
import { resolvePath } from '../util/paths.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prop values arrive as literals, bindings (`{ path }`) or function calls
|
|
10
|
+
* (`{ call }`). Resolving them is what makes a node written as data behave
|
|
11
|
+
* exactly like one written in JSX.
|
|
12
|
+
*
|
|
13
|
+
* Every path read during a resolve is recorded, so the caller can subscribe to
|
|
14
|
+
* precisely the paths this subtree depends on and re-render on nothing else.
|
|
15
|
+
*/
|
|
16
|
+
export interface ResolveContext {
|
|
17
|
+
store: ReactiveStore;
|
|
18
|
+
/** Relative paths resolve against this. */
|
|
19
|
+
dataContext?: BindingPath;
|
|
20
|
+
/** Run a registered command. */
|
|
21
|
+
execute(id: string, args?: Record<string, unknown>): unknown;
|
|
22
|
+
/** Publish on an event path. */
|
|
23
|
+
emit(path: string, payload?: unknown): void;
|
|
24
|
+
/** Collected during the resolve; the caller subscribes to these. */
|
|
25
|
+
reads: Set<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `#/a/b` means: the value AT this path is itself a path. One extra hop. */
|
|
29
|
+
function readPath(ctx: ResolveContext, raw: string): unknown {
|
|
30
|
+
let path = raw;
|
|
31
|
+
|
|
32
|
+
// `{{ /other/path }}` substitutes another path's value into this one first.
|
|
33
|
+
if (path.includes('{{')) {
|
|
34
|
+
path = path.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_m, inner: string) => {
|
|
35
|
+
const resolved = resolvePath(inner.trim(), ctx.dataContext);
|
|
36
|
+
ctx.reads.add(resolved);
|
|
37
|
+
return String(ctx.store.get(resolved) ?? '');
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (path.startsWith('#/')) {
|
|
42
|
+
const indirect = resolvePath(path.slice(1), ctx.dataContext);
|
|
43
|
+
ctx.reads.add(indirect);
|
|
44
|
+
const target = ctx.store.get(indirect);
|
|
45
|
+
if (typeof target !== 'string') return undefined;
|
|
46
|
+
const final = resolvePath(target, ctx.dataContext);
|
|
47
|
+
ctx.reads.add(final);
|
|
48
|
+
return ctx.store.get(final);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolved = resolvePath(path, ctx.dataContext);
|
|
52
|
+
ctx.reads.add(resolved);
|
|
53
|
+
return ctx.store.get(resolved);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveCall(ctx: ResolveContext, call: FunctionCall): unknown {
|
|
57
|
+
const args: Record<string, unknown> = {};
|
|
58
|
+
for (const [k, v] of Object.entries(call.args ?? {})) {
|
|
59
|
+
args[k] = resolveValue(ctx, v);
|
|
60
|
+
}
|
|
61
|
+
return ctx.execute(call.call, args);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Turn an action into something callable. Actions stay data right up to the
|
|
66
|
+
* moment they are invoked, which is what lets the same button work whether it
|
|
67
|
+
* was written in JSX with a closure or loaded from JSON naming a command.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveAction(
|
|
70
|
+
ctx: ResolveContext,
|
|
71
|
+
action: Action | ((...args: unknown[]) => unknown) | undefined,
|
|
72
|
+
): ((...args: unknown[]) => unknown) | undefined {
|
|
73
|
+
if (action === undefined) return undefined;
|
|
74
|
+
if (typeof action === 'function') return action;
|
|
75
|
+
if ('handler' in action) return action.handler as (...args: unknown[]) => unknown;
|
|
76
|
+
if ('functionCall' in action) {
|
|
77
|
+
return () => resolveCall(ctx, action.functionCall);
|
|
78
|
+
}
|
|
79
|
+
if ('emit' in action) {
|
|
80
|
+
return () => ctx.emit(action.emit.path, resolveValue(ctx, action.emit.payload));
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Resolve one prop value, leaving nodes and closures alone. */
|
|
86
|
+
export function resolveValue(ctx: ResolveContext, value: unknown): unknown {
|
|
87
|
+
if (value === null || value === undefined) return value;
|
|
88
|
+
if (typeof value !== 'object') return value;
|
|
89
|
+
if (typeof value === 'function') return value;
|
|
90
|
+
|
|
91
|
+
if (isDataBinding(value)) return readPath(ctx, (value as DataBinding).path);
|
|
92
|
+
if (isFunctionCall(value)) return resolveCall(ctx, value as FunctionCall);
|
|
93
|
+
// Nodes and templated child lists stay as they are; the reconciler owns them.
|
|
94
|
+
if (isComponentNode(value)) return value;
|
|
95
|
+
if (isAction(value)) return value;
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(value)) return value.map((v) => resolveValue(ctx, v));
|
|
98
|
+
|
|
99
|
+
const out: Record<string, unknown> = {};
|
|
100
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
101
|
+
out[k] = resolveValue(ctx, v);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve every prop on a node. `component`, `id`, `key` and `$meta` are
|
|
108
|
+
* structural and pass through untouched.
|
|
109
|
+
*
|
|
110
|
+
* A prop named `onSomething` whose value is an action becomes a callable, so a
|
|
111
|
+
* node loaded from JSON can say `{ onPress: { functionCall: { call: 'x' } } }`
|
|
112
|
+
* and the component receives the same shape it would get from JSX. Without
|
|
113
|
+
* this, actions-as-data would only work on the handful of props the runtime
|
|
114
|
+
* knew about by name.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveProps(
|
|
117
|
+
ctx: ResolveContext,
|
|
118
|
+
node: ComponentNode,
|
|
119
|
+
): Record<string, unknown> {
|
|
120
|
+
const out: Record<string, unknown> = {};
|
|
121
|
+
for (const [k, v] of Object.entries(node)) {
|
|
122
|
+
if (k === 'component' || k === '$meta' || k === 'key') continue;
|
|
123
|
+
if (k === 'children') {
|
|
124
|
+
out.children = v;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (isHandlerProp(k) && isAction(v)) {
|
|
128
|
+
out[k] = resolveAction(ctx, v);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
out[k] = resolveValue(ctx, v);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `onPress`, `onSelect`, `onChange` - but not `once` or `only`. */
|
|
137
|
+
function isHandlerProp(key: string): boolean {
|
|
138
|
+
return key.length > 2 && key.startsWith('on') && key[2] === key[2]?.toUpperCase();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** True when the value is a templated child list: one instance per item. */
|
|
142
|
+
export function isTemplateChildren(
|
|
143
|
+
v: unknown,
|
|
144
|
+
): v is { template: ComponentNode; path: BindingPath } {
|
|
145
|
+
return (
|
|
146
|
+
typeof v === 'object' && v !== null &&
|
|
147
|
+
'template' in v && 'path' in v &&
|
|
148
|
+
isComponentNode((v as { template: unknown }).template)
|
|
149
|
+
);
|
|
150
|
+
}
|