@typecad/ui 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +1443 -0
- package/dist/engine-index.d.ts +7 -0
- package/dist/engine-index.js +41 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +42 -0
- package/dist/preview/build-program.d.ts +7 -0
- package/dist/preview/build-program.js +665 -0
- package/dist/preview/host-gfx.d.ts +90 -0
- package/dist/preview/host-gfx.js +552 -0
- package/dist/preview/host-ui-runtime.d.ts +241 -0
- package/dist/preview/host-ui-runtime.js +3236 -0
- package/dist/preview/types.d.ts +83 -0
- package/dist/preview/types.js +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +10 -0
- package/dist/ui-engine/block-layout.d.ts +8 -0
- package/dist/ui-engine/block-layout.js +95 -0
- package/dist/ui-engine/color.d.ts +49 -0
- package/dist/ui-engine/color.js +273 -0
- package/dist/ui-engine/css-parser.d.ts +155 -0
- package/dist/ui-engine/css-parser.js +1058 -0
- package/dist/ui-engine/default-keyboards.d.ts +3 -0
- package/dist/ui-engine/default-keyboards.js +53 -0
- package/dist/ui-engine/easing.d.ts +12 -0
- package/dist/ui-engine/easing.js +76 -0
- package/dist/ui-engine/font-assets.d.ts +50 -0
- package/dist/ui-engine/font-assets.js +431 -0
- package/dist/ui-engine/html-parser.d.ts +116 -0
- package/dist/ui-engine/html-parser.js +284 -0
- package/dist/ui-engine/image-assets.d.ts +21 -0
- package/dist/ui-engine/image-assets.js +125 -0
- package/dist/ui-engine/inline-parser.d.ts +30 -0
- package/dist/ui-engine/inline-parser.js +66 -0
- package/dist/ui-engine/keyframes.d.ts +3 -0
- package/dist/ui-engine/keyframes.js +39 -0
- package/dist/ui-engine/layout-engine.d.ts +35 -0
- package/dist/ui-engine/layout-engine.js +203 -0
- package/dist/ui-engine/model.d.ts +239 -0
- package/dist/ui-engine/model.js +987 -0
- package/dist/ui-engine/rich-layout.d.ts +51 -0
- package/dist/ui-engine/rich-layout.js +220 -0
- package/dist/ui-engine/run-types.d.ts +13 -0
- package/dist/ui-engine/run-types.js +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
- package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
- package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
- package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
- package/dist/ui-engine/runtime-header/display-shim.js +104 -0
- package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
- package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
- package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
- package/dist/ui-engine/runtime-header/guard-close.js +8 -0
- package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
- package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
- package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
- package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
- package/dist/ui-engine/runtime-header/keyboard.js +342 -0
- package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
- package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
- package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
- package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
- package/dist/ui-engine/runtime-header/structs.js +220 -0
- package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
- package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
- package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick.js +17 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
- package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
- package/dist/ui-engine/runtime-header/types-defines.js +79 -0
- package/dist/ui-engine/runtime-header.d.ts +1 -0
- package/dist/ui-engine/runtime-header.js +69 -0
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
- package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
- package/dist/ui-engine/select-engine.d.ts +4 -0
- package/dist/ui-engine/select-engine.js +19 -0
- package/dist/ui-engine/style-resolver.d.ts +77 -0
- package/dist/ui-engine/style-resolver.js +341 -0
- package/dist/ui-engine/text-layout.d.ts +19 -0
- package/dist/ui-engine/text-layout.js +92 -0
- package/dist/ui-engine/transpile-ui.d.ts +11 -0
- package/dist/ui-engine/transpile-ui.js +33 -0
- package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
- package/dist/ui-engine/ua-stylesheet.js +56 -0
- package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
- package/dist/ui-engine/ui-file-splitter.js +47 -0
- package/dist/ui-engine/ui-lowering.d.ts +33 -0
- package/dist/ui-engine/ui-lowering.js +390 -0
- package/dist/ui-engine/ui-registry.d.ts +81 -0
- package/dist/ui-engine/ui-registry.js +429 -0
- package/dist/ui-engine/yoga-layout.d.ts +12 -0
- package/dist/ui-engine/yoga-layout.js +314 -0
- package/package.json +86 -0
- package/src/engine-index.ts +51 -0
- package/src/index.ts +232 -0
- package/src/preview/build-program.ts +734 -0
- package/src/preview/host-gfx.ts +580 -0
- package/src/preview/host-ui-runtime.ts +3245 -0
- package/src/preview/types.ts +96 -0
- package/src/types.ts +127 -0
- package/src/ui-engine/block-layout.ts +104 -0
- package/src/ui-engine/color.ts +263 -0
- package/src/ui-engine/css-parser.ts +1003 -0
- package/src/ui-engine/default-keyboards.ts +58 -0
- package/src/ui-engine/easing.ts +53 -0
- package/src/ui-engine/font-assets.ts +512 -0
- package/src/ui-engine/html-parser.ts +397 -0
- package/src/ui-engine/image-assets.ts +170 -0
- package/src/ui-engine/inline-parser.ts +78 -0
- package/src/ui-engine/keyframes.ts +47 -0
- package/src/ui-engine/layout-engine.ts +228 -0
- package/src/ui-engine/model.ts +1225 -0
- package/src/ui-engine/rich-layout.ts +257 -0
- package/src/ui-engine/run-types.ts +14 -0
- package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
- package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
- package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
- package/src/ui-engine/runtime-header/display-shim.ts +104 -0
- package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
- package/src/ui-engine/runtime-header/guard-close.ts +8 -0
- package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
- package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
- package/src/ui-engine/runtime-header/keyboard.ts +342 -0
- package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
- package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
- package/src/ui-engine/runtime-header/structs.ts +220 -0
- package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
- package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
- package/src/ui-engine/runtime-header/tick.ts +18 -0
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
- package/src/ui-engine/runtime-header/types-defines.ts +79 -0
- package/src/ui-engine/runtime-header.ts +71 -0
- package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
- package/src/ui-engine/select-engine.ts +21 -0
- package/src/ui-engine/style-resolver.ts +416 -0
- package/src/ui-engine/text-layout.ts +114 -0
- package/src/ui-engine/transpile-ui.ts +45 -0
- package/src/ui-engine/ua-stylesheet.ts +60 -0
- package/src/ui-engine/ui-file-splitter.ts +59 -0
- package/src/ui-engine/ui-lowering.ts +459 -0
- package/src/ui-engine/ui-registry.ts +530 -0
- package/src/ui-engine/yoga-layout.ts +309 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { effectiveDisplaySize, resolveDisplayProfile } from "@typecad/cuttlefish/api/shared";
|
|
5
|
+
import { parseCss, parseFontFaces, parseKeyframes } from "../ui-engine/css-parser.js";
|
|
6
|
+
import { extractStyleBlocks, parseHtmlWithKeyboards } from "../ui-engine/html-parser.js";
|
|
7
|
+
import { splitUiFile } from "../ui-engine/ui-file-splitter.js";
|
|
8
|
+
import { buildUIFontAssets } from "../ui-engine/font-assets.js";
|
|
9
|
+
import { loadImageAssets } from "../ui-engine/image-assets.js";
|
|
10
|
+
import { buildKeyframeSets } from "../ui-engine/keyframes.js";
|
|
11
|
+
import { measureWithFonts } from "../ui-engine/layout-engine.js";
|
|
12
|
+
import { lowerUIToModel } from "../ui-engine/model.js";
|
|
13
|
+
import { selectEngine } from "../ui-engine/select-engine.js";
|
|
14
|
+
import { resolveStyles } from "../ui-engine/style-resolver.js";
|
|
15
|
+
import { getThemeClass, setThemeClass } from "@typecad/cuttlefish/stores/theme-store";
|
|
16
|
+
function nodeIndexById(programNodes, id) {
|
|
17
|
+
const found = programNodes.find((node) => node.id === id);
|
|
18
|
+
return found?.id ? programNodes.indexOf(found) : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Convert a `{expr}` interpolation text into a TS template-literal expression
|
|
21
|
+
* string the preview can evaluate. E.g. `taps: {count}` → `` `taps: ${count}` ``.
|
|
22
|
+
* Mirrors lowerInterpolationText's scanning but produces a JS expression rather
|
|
23
|
+
* than a snprintf format string. Returns undefined when there's no interpolation.
|
|
24
|
+
*
|
|
25
|
+
* Each interpolation is wrapped in a boolean→number coercion because hardware
|
|
26
|
+
* lowers HTML `{expr}` interpolations via snprintf("%d") (numericFormat default;
|
|
27
|
+
* see lowerInterpolationText), so a bool prints as 1/0 on the device. JS
|
|
28
|
+
* `${true}` would otherwise stringify as "true". The coercion is a no-op for
|
|
29
|
+
* numbers and strings (Number("x") is NaN, but the HTML {expr} path is
|
|
30
|
+
* documented numeric-only — string-signal interpolation belongs to the
|
|
31
|
+
* ui.bind path, which has type information). */
|
|
32
|
+
function interpolationToExpression(raw) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
let hasInterp = false;
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < raw.length) {
|
|
37
|
+
const ch = raw[i];
|
|
38
|
+
if (ch === "{") {
|
|
39
|
+
let depth = 1;
|
|
40
|
+
let j = i + 1;
|
|
41
|
+
while (j < raw.length && depth > 0) {
|
|
42
|
+
if (raw[j] === "{")
|
|
43
|
+
depth++;
|
|
44
|
+
else if (raw[j] === "}")
|
|
45
|
+
depth--;
|
|
46
|
+
if (depth === 0)
|
|
47
|
+
break;
|
|
48
|
+
j++;
|
|
49
|
+
}
|
|
50
|
+
const content = raw.slice(i + 1, j);
|
|
51
|
+
if (depth === 0 && content.length > 0 && !content.includes("{")) {
|
|
52
|
+
hasInterp = true;
|
|
53
|
+
// Coerce via Number(): HTML {expr} interpolations lower to snprintf
|
|
54
|
+
// "%d" on hardware (numericFormat default), so bool→1/0 and the numeric
|
|
55
|
+
// value passes through unchanged. Number(5)===5, Number(true)===1.
|
|
56
|
+
parts.push("${Number(" + content.trim() + ")}");
|
|
57
|
+
i = j + 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Literal text — escape backticks and ${ in the literal parts.
|
|
62
|
+
parts.push(ch === "`" ? "\\`" : (ch === "$" && raw[i + 1] === "{") ? "\\${" : ch);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
if (!hasInterp)
|
|
66
|
+
return undefined;
|
|
67
|
+
return "`" + parts.join("") + "`";
|
|
68
|
+
}
|
|
69
|
+
/** Walk styled trees for {expr} interpolation nodes and synthesize preview text
|
|
70
|
+
* bindings (mirroring the runtime's auto-wire synthesis). Each produces a
|
|
71
|
+
* PreviewBindingSpec whose expression is a template-literal the preview evals. */
|
|
72
|
+
function collectInterpolationBindings(trees, programNodes) {
|
|
73
|
+
const out = [];
|
|
74
|
+
const walk = (node) => {
|
|
75
|
+
if (node.hasInterpolation && node.text) {
|
|
76
|
+
const expression = interpolationToExpression(node.text);
|
|
77
|
+
if (expression) {
|
|
78
|
+
// Resolve nodeIndex by id; interpolation nodes may lack an id (when the
|
|
79
|
+
// text references a signal directly), but the demo keeps the id for CSS.
|
|
80
|
+
const nodeIndex = node.id ? nodeIndexById(programNodes, node.id) : undefined;
|
|
81
|
+
if (nodeIndex !== undefined) {
|
|
82
|
+
out.push({ nodeId: node.id ?? `__interp_${nodeIndex}`, nodeIndex, property: "text", expression });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
node.children?.forEach(walk);
|
|
87
|
+
};
|
|
88
|
+
trees.forEach(walk);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** Walk styled trees for on:* declarative event handlers and synthesize preview
|
|
92
|
+
* callback specs. Each on:click="saveSettings" becomes the named function's
|
|
93
|
+
* body when the source is available, so preview callbacks execute the same
|
|
94
|
+
* author code the device emits as a standalone function. */
|
|
95
|
+
function collectEventCallbacks(trees, programNodes, namedFunctionBodies) {
|
|
96
|
+
const out = [];
|
|
97
|
+
const walk = (node) => {
|
|
98
|
+
if (node.events) {
|
|
99
|
+
const nodeIndex = node.id ? nodeIndexById(programNodes, node.id) : undefined;
|
|
100
|
+
if (nodeIndex !== undefined) {
|
|
101
|
+
for (const kind of ["click", "hold", "release", "change"]) {
|
|
102
|
+
const fn = node.events[kind];
|
|
103
|
+
if (fn) {
|
|
104
|
+
out.push({ nodeId: node.id ?? `__event_${nodeIndex}`, nodeIndex, kind, body: namedFunctionBodies.get(fn) ?? `${fn}()` });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
node.children?.forEach(walk);
|
|
110
|
+
};
|
|
111
|
+
trees.forEach(walk);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** Walk styled trees for bind:* declarative two-way bindings. Returns the READ
|
|
115
|
+
* half (signal → node) as preview bindings AND the WRITE half (node → signal)
|
|
116
|
+
* as preview callbacks. bind:text write-back fires on the keyboard-commit
|
|
117
|
+
* "change" dispatch (keyboardClose → dispatch("change", target)); the callback
|
|
118
|
+
* body reads the committed text via screen.<id>.text and calls signal.set.
|
|
119
|
+
* bind:value write-back fires on the check "click" or range "rangechange"
|
|
120
|
+
* event — matching the runtime kinds in ui-element-auto-wire.ts:136 so the
|
|
121
|
+
* preview dispatches them on the same gestures the device does. */
|
|
122
|
+
function collectBindBindings(trees, programNodes) {
|
|
123
|
+
const bindings = [];
|
|
124
|
+
const callbacks = [];
|
|
125
|
+
const walk = (node) => {
|
|
126
|
+
if (node.bind) {
|
|
127
|
+
const nodeIndex = node.id ? nodeIndexById(programNodes, node.id) : undefined;
|
|
128
|
+
if (nodeIndex !== undefined && node.id) {
|
|
129
|
+
if (node.bind.text) {
|
|
130
|
+
// Read: signal → input text.
|
|
131
|
+
bindings.push({ nodeId: node.id, nodeIndex, property: "text", expression: node.bind.text });
|
|
132
|
+
// Write: keyboard commit → signal.set(committed text).
|
|
133
|
+
callbacks.push({ nodeId: node.id, nodeIndex, kind: "change", body: `${node.bind.text}.set(screen.${node.id}.text)` });
|
|
134
|
+
}
|
|
135
|
+
if (node.bind.value) {
|
|
136
|
+
// Read: signal → node value.
|
|
137
|
+
bindings.push({ nodeId: node.id, nodeIndex, property: "value", expression: node.bind.value });
|
|
138
|
+
// Write: range drag (rangechange) / check toggle (click) → signal.set(value).
|
|
139
|
+
// Kinds must match the runtime so the preview fires on the same gesture.
|
|
140
|
+
const valueKind = node.tag === "check" ? "click" : "rangechange";
|
|
141
|
+
callbacks.push({ nodeId: node.id, nodeIndex, kind: valueKind, body: `${node.bind.value}.set(screen.${node.id}.value)` });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
node.children?.forEach(walk);
|
|
146
|
+
};
|
|
147
|
+
trees.forEach(walk);
|
|
148
|
+
return { bindings, callbacks };
|
|
149
|
+
}
|
|
150
|
+
async function loadProfileRegistry(frameworkPackage) {
|
|
151
|
+
const registry = new Map();
|
|
152
|
+
if (!frameworkPackage)
|
|
153
|
+
return registry;
|
|
154
|
+
const profileMod = await import(frameworkPackage + "/displays/ili9341-spi").catch(() => null);
|
|
155
|
+
if (profileMod?.BUILT_IN_PROFILES) {
|
|
156
|
+
for (const [key, value] of Object.entries(profileMod.BUILT_IN_PROFILES)) {
|
|
157
|
+
registry.set(key, value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return registry;
|
|
161
|
+
}
|
|
162
|
+
function loadFont(projectRoot, diagnostics) {
|
|
163
|
+
const fontPath = path.join(projectRoot, "lib", "Adafruit_GFX_Library", "glcdfont.c");
|
|
164
|
+
if (!fs.existsSync(fontPath)) {
|
|
165
|
+
diagnostics.push({
|
|
166
|
+
severity: "warning",
|
|
167
|
+
message: `Default GFX font not found at ${fontPath}; text pixels will be blank.`,
|
|
168
|
+
});
|
|
169
|
+
return Array.from(new Uint8Array(1280));
|
|
170
|
+
}
|
|
171
|
+
const source = fs.readFileSync(fontPath, "utf-8");
|
|
172
|
+
const match = /font\[\]\s+PROGMEM\s*=\s*\{([\s\S]*?)\};/.exec(source);
|
|
173
|
+
const body = match?.[1] ?? source;
|
|
174
|
+
const bytes = [...body.matchAll(/0x([0-9a-fA-F]{1,2})/g)].map((m) => parseInt(m[1], 16));
|
|
175
|
+
if (bytes.length < 1280) {
|
|
176
|
+
diagnostics.push({
|
|
177
|
+
severity: "warning",
|
|
178
|
+
message: `Parsed ${bytes.length} font bytes from ${fontPath}; expected 1280.`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return bytes.slice(0, 1280);
|
|
182
|
+
}
|
|
183
|
+
function callbackBodyText(cb, source) {
|
|
184
|
+
if (!cb || (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb)))
|
|
185
|
+
return "";
|
|
186
|
+
if (ts.isExpression(cb.body))
|
|
187
|
+
return `${cb.body.getText(source)};`;
|
|
188
|
+
return cb.body.statements.map((statement) => statement.getText(source)).join("\n");
|
|
189
|
+
}
|
|
190
|
+
function collectNamedFunctionBodies(source) {
|
|
191
|
+
const bodies = new Map();
|
|
192
|
+
for (const statement of source.statements) {
|
|
193
|
+
if (!ts.isFunctionDeclaration(statement) || !statement.name || !statement.body)
|
|
194
|
+
continue;
|
|
195
|
+
bodies.set(statement.name.text, statement.body.statements.map((s) => s.getText(source)).join("\n"));
|
|
196
|
+
}
|
|
197
|
+
return bodies;
|
|
198
|
+
}
|
|
199
|
+
function callbackExpressionText(cb, source) {
|
|
200
|
+
if (!cb || (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb)))
|
|
201
|
+
return undefined;
|
|
202
|
+
if (ts.isExpression(cb.body))
|
|
203
|
+
return cb.body.getText(source);
|
|
204
|
+
for (const statement of cb.body.statements) {
|
|
205
|
+
if (ts.isReturnStatement(statement) && statement.expression) {
|
|
206
|
+
return statement.expression.getText(source);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
function callbackFirstParamName(cb) {
|
|
212
|
+
if (!cb || (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb)))
|
|
213
|
+
return undefined;
|
|
214
|
+
const param = cb.parameters[0];
|
|
215
|
+
if (!param || !ts.isIdentifier(param.name))
|
|
216
|
+
return undefined;
|
|
217
|
+
return param.name.text;
|
|
218
|
+
}
|
|
219
|
+
function numericArgText(arg, source, fallback) {
|
|
220
|
+
if (!arg)
|
|
221
|
+
return fallback;
|
|
222
|
+
if (ts.isNumericLiteral(arg))
|
|
223
|
+
return arg.text;
|
|
224
|
+
if (ts.isIdentifier(arg))
|
|
225
|
+
return arg.text;
|
|
226
|
+
return arg.getText(source);
|
|
227
|
+
}
|
|
228
|
+
function readTreeElement(expr) {
|
|
229
|
+
if (!ts.isPropertyAccessExpression(expr) || !ts.isIdentifier(expr.expression))
|
|
230
|
+
return undefined;
|
|
231
|
+
return { treeName: expr.expression.text, elemId: expr.name.text };
|
|
232
|
+
}
|
|
233
|
+
function readTreeElementMethod(call) {
|
|
234
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
235
|
+
return undefined;
|
|
236
|
+
const method = call.expression.name.text;
|
|
237
|
+
const receiver = call.expression.expression;
|
|
238
|
+
const element = readTreeElement(receiver);
|
|
239
|
+
return element ? { ...element, method } : undefined;
|
|
240
|
+
}
|
|
241
|
+
function findUIModuleImports(source, entryDir, diagnostics) {
|
|
242
|
+
const imports = [];
|
|
243
|
+
for (const statement of source.statements) {
|
|
244
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
245
|
+
continue;
|
|
246
|
+
const specifier = statement.moduleSpecifier.text;
|
|
247
|
+
if (specifier.endsWith(".ui.html")) {
|
|
248
|
+
if (statement.importClause?.name) {
|
|
249
|
+
imports.push({
|
|
250
|
+
treeName: statement.importClause.name.text,
|
|
251
|
+
htmlPath: path.resolve(entryDir, specifier),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const named = statement.importClause?.namedBindings;
|
|
255
|
+
if (named && ts.isNamedImports(named)) {
|
|
256
|
+
for (const element of named.elements) {
|
|
257
|
+
imports.push({
|
|
258
|
+
treeName: element.name.text,
|
|
259
|
+
htmlPath: path.resolve(entryDir, specifier),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (specifier.includes("/lib/") || specifier.startsWith("../lib/") || specifier.startsWith("./lib/")) {
|
|
265
|
+
diagnostics.push({
|
|
266
|
+
severity: "info",
|
|
267
|
+
message: `Preview is stubbing embedded-only import "${specifier}".`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return imports;
|
|
272
|
+
}
|
|
273
|
+
function themeCssPath(displayThemeCss, htmlPath, configDir) {
|
|
274
|
+
if (!displayThemeCss)
|
|
275
|
+
return htmlPath.replace(/\.ui\.html$/, ".ui.css");
|
|
276
|
+
return path.isAbsolute(displayThemeCss)
|
|
277
|
+
? displayThemeCss
|
|
278
|
+
: path.resolve(configDir, displayThemeCss);
|
|
279
|
+
}
|
|
280
|
+
function collectHrefCallbacks(screens, programNodes) {
|
|
281
|
+
const screenIds = new Map();
|
|
282
|
+
screens.forEach((screen, index) => {
|
|
283
|
+
if (screen.id)
|
|
284
|
+
screenIds.set(screen.id, index);
|
|
285
|
+
});
|
|
286
|
+
const callbacks = [];
|
|
287
|
+
// Walk in document order with a running index, matching how lowerUIToModel
|
|
288
|
+
// assigns node indices (flatten(): index = cursor.i++ for every node, depth-
|
|
289
|
+
// first). The cursor advances by each subtree's size, so a child's index is
|
|
290
|
+
// always (parent + 1 + sum of earlier siblings' subtree sizes). This lets us
|
|
291
|
+
// resolve id-less <a href> links the same way the device does
|
|
292
|
+
// (ui-element-auto-wire.ts wires any node with href, id or not).
|
|
293
|
+
const visit = (node, nodeIndex) => {
|
|
294
|
+
if (node.href) {
|
|
295
|
+
const target = node.href.startsWith("#") ? node.href.slice(1) : node.href;
|
|
296
|
+
const targetScreen = screenIds.get(target);
|
|
297
|
+
if (targetScreen !== undefined && nodeIndex < programNodes.length) {
|
|
298
|
+
callbacks.push({
|
|
299
|
+
nodeId: node.id ?? `__ui_link${nodeIndex}_nav`,
|
|
300
|
+
nodeIndex,
|
|
301
|
+
kind: "click",
|
|
302
|
+
body: `ui.navigate(${targetScreen});`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else if (node.runs?.some(r => r.href)) {
|
|
307
|
+
// Run-bearing link node (inline <a href> inside a paragraph). The link
|
|
308
|
+
// targets are resolved into the model at lower time; the preview tap path
|
|
309
|
+
// calls richLinkHit to pick the target. Register a no-op click callback so
|
|
310
|
+
// the node is hit-testable.
|
|
311
|
+
if (nodeIndex < programNodes.length) {
|
|
312
|
+
callbacks.push({
|
|
313
|
+
nodeId: node.id ?? `__ui_richlink${nodeIndex}_nav`,
|
|
314
|
+
nodeIndex,
|
|
315
|
+
kind: "click",
|
|
316
|
+
body: `/* rich-text link; target resolved by richLinkHit */`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
let nextIndex = nodeIndex + 1;
|
|
321
|
+
for (const child of node.children)
|
|
322
|
+
nextIndex = visit(child, nextIndex);
|
|
323
|
+
return nextIndex;
|
|
324
|
+
};
|
|
325
|
+
// Each screen is a contiguous subtree in the flattened node table; find where
|
|
326
|
+
// screen i starts (first node with that screenId) and walk its styled tree.
|
|
327
|
+
for (let i = 0; i < screens.length; i++) {
|
|
328
|
+
const startIndex = programNodes.findIndex((n) => n.screenId === i);
|
|
329
|
+
if (startIndex >= 0)
|
|
330
|
+
visit(screens[i], startIndex);
|
|
331
|
+
}
|
|
332
|
+
return callbacks;
|
|
333
|
+
}
|
|
334
|
+
function extractAuthorSpecs(source, uiImports, programNodes) {
|
|
335
|
+
const diagnostics = [];
|
|
336
|
+
const importedTrees = new Set(uiImports.map((imp) => imp.treeName));
|
|
337
|
+
const bindings = [];
|
|
338
|
+
const listBindings = [];
|
|
339
|
+
const callbacks = [];
|
|
340
|
+
const initialAssignments = [];
|
|
341
|
+
const intervals = [];
|
|
342
|
+
const pinControls = [];
|
|
343
|
+
const canvasBindings = [];
|
|
344
|
+
const moduleVars = [];
|
|
345
|
+
const resolveNode = (treeName, elemId) => {
|
|
346
|
+
if (!importedTrees.has(treeName))
|
|
347
|
+
return undefined;
|
|
348
|
+
const idx = nodeIndexById(programNodes, elemId);
|
|
349
|
+
if (idx === undefined) {
|
|
350
|
+
diagnostics.push({ severity: "warning", message: `Preview could not find UI element "${treeName}.${elemId}".` });
|
|
351
|
+
}
|
|
352
|
+
return idx;
|
|
353
|
+
};
|
|
354
|
+
for (const statement of source.statements) {
|
|
355
|
+
// Top-level `let`/`const`/`var` declarations become module-scoped bindings
|
|
356
|
+
// the device hoists to C++ globals. Capture each declared name + initializer
|
|
357
|
+
// so callback bodies (setInterval, onClick, ui.bind, ...) that reference them
|
|
358
|
+
// resolve at preview runtime instead of throwing ReferenceError.
|
|
359
|
+
if (ts.isVariableStatement(statement)) {
|
|
360
|
+
for (const decl of statement.declarationList.declarations) {
|
|
361
|
+
if (ts.isIdentifier(decl.name)) {
|
|
362
|
+
moduleVars.push({
|
|
363
|
+
name: decl.name.text,
|
|
364
|
+
initializer: decl.initializer ? decl.initializer.getText(source) : undefined,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (ts.isExpressionStatement(statement) && ts.isBinaryExpression(statement.expression)) {
|
|
371
|
+
const expr = statement.expression;
|
|
372
|
+
if (expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
373
|
+
ts.isPropertyAccessExpression(expr.left) &&
|
|
374
|
+
expr.left.name.text === "value") {
|
|
375
|
+
const element = readTreeElement(expr.left.expression);
|
|
376
|
+
if (element) {
|
|
377
|
+
const nodeIndex = resolveNode(element.treeName, element.elemId);
|
|
378
|
+
if (nodeIndex !== undefined) {
|
|
379
|
+
initialAssignments.push({
|
|
380
|
+
nodeId: element.elemId,
|
|
381
|
+
nodeIndex,
|
|
382
|
+
expression: expr.right.getText(source),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression))
|
|
390
|
+
continue;
|
|
391
|
+
const call = statement.expression;
|
|
392
|
+
if (ts.isIdentifier(call.expression) && call.expression.text === "setInterval") {
|
|
393
|
+
const delay = call.arguments[1] && ts.isNumericLiteral(call.arguments[1])
|
|
394
|
+
? Number(call.arguments[1].text)
|
|
395
|
+
: 0;
|
|
396
|
+
intervals.push({ body: callbackBodyText(call.arguments[0], source), delayMs: delay });
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)) {
|
|
400
|
+
const objectName = call.expression.expression.text;
|
|
401
|
+
const method = call.expression.name.text;
|
|
402
|
+
if (objectName === "ui" && method === "bind") {
|
|
403
|
+
const elementArg = call.arguments[0];
|
|
404
|
+
const propArg = call.arguments[1];
|
|
405
|
+
const fnArg = call.arguments[2];
|
|
406
|
+
const element = elementArg ? readTreeElement(elementArg) : undefined;
|
|
407
|
+
const property = propArg && ts.isStringLiteral(propArg) ? propArg.text : "text";
|
|
408
|
+
const expression = callbackExpressionText(fnArg, source);
|
|
409
|
+
if (element && expression) {
|
|
410
|
+
const nodeIndex = resolveNode(element.treeName, element.elemId);
|
|
411
|
+
if (nodeIndex !== undefined) {
|
|
412
|
+
bindings.push({ nodeId: element.elemId, nodeIndex, property, expression });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (objectName === "ui" && method === "bindList") {
|
|
418
|
+
const elementArg = call.arguments[0];
|
|
419
|
+
const countArg = call.arguments[1];
|
|
420
|
+
const itemArg = call.arguments[2];
|
|
421
|
+
const tapArg = call.arguments[3];
|
|
422
|
+
const element = elementArg ? readTreeElement(elementArg) : undefined;
|
|
423
|
+
const countExpression = callbackExpressionText(countArg, source);
|
|
424
|
+
const itemExpression = callbackExpressionText(itemArg, source);
|
|
425
|
+
if (element && countExpression && itemExpression) {
|
|
426
|
+
const nodeIndex = resolveNode(element.treeName, element.elemId);
|
|
427
|
+
if (nodeIndex !== undefined) {
|
|
428
|
+
listBindings.push({
|
|
429
|
+
nodeId: element.elemId,
|
|
430
|
+
nodeIndex,
|
|
431
|
+
countExpression,
|
|
432
|
+
itemExpression,
|
|
433
|
+
itemParam: callbackFirstParamName(itemArg),
|
|
434
|
+
tapBody: tapArg ? callbackBodyText(tapArg, source) : undefined,
|
|
435
|
+
tapParam: callbackFirstParamName(tapArg),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (objectName === "ui" && method === "bindInput") {
|
|
442
|
+
// ui.bindInput(node, cb): cb fires with the input's committed text on
|
|
443
|
+
// keyboard close. Collect as a "change" callback (the keyboard-commit
|
|
444
|
+
// dispatch kind) and record the cb's first param so the dispatch path
|
|
445
|
+
// can bind the text to it as a local. Mirrors resolveBindInputCall,
|
|
446
|
+
// which renames the arrow's param to `text` in the lowered C++.
|
|
447
|
+
const elementArg = call.arguments[0];
|
|
448
|
+
const cbArg = call.arguments[1];
|
|
449
|
+
const element = elementArg ? readTreeElement(elementArg) : undefined;
|
|
450
|
+
if (element && cbArg && (ts.isArrowFunction(cbArg) || ts.isFunctionExpression(cbArg))) {
|
|
451
|
+
const nodeIndex = resolveNode(element.treeName, element.elemId);
|
|
452
|
+
if (nodeIndex !== undefined) {
|
|
453
|
+
const param = callbackFirstParamName(cbArg);
|
|
454
|
+
callbacks.push({
|
|
455
|
+
nodeId: element.elemId,
|
|
456
|
+
nodeIndex,
|
|
457
|
+
kind: "change",
|
|
458
|
+
body: callbackBodyText(cbArg, source),
|
|
459
|
+
// The runtime renames the param to `text`; record the author's
|
|
460
|
+
// name so we can bind the committed text under whichever name the
|
|
461
|
+
// body actually references. If absent, default to `text`.
|
|
462
|
+
param: param ?? "text",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (objectName === "ui" && method === "drawCanvas") {
|
|
469
|
+
const elementArg = call.arguments[0];
|
|
470
|
+
const cbArg = call.arguments[1];
|
|
471
|
+
const element = elementArg ? readTreeElement(elementArg) : undefined;
|
|
472
|
+
if (element && cbArg && (ts.isArrowFunction(cbArg) || ts.isFunctionExpression(cbArg))) {
|
|
473
|
+
const nodeIndex = resolveNode(element.treeName, element.elemId);
|
|
474
|
+
if (nodeIndex !== undefined) {
|
|
475
|
+
canvasBindings.push({
|
|
476
|
+
nodeId: element.elemId,
|
|
477
|
+
nodeIndex,
|
|
478
|
+
drawBody: callbackBodyText(cbArg, source),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (objectName === "ui" && method === "watchPin") {
|
|
485
|
+
const pin = numericArgText(call.arguments[0], source, "0");
|
|
486
|
+
pinControls.push({
|
|
487
|
+
label: `ui.watchPin(${pin})`,
|
|
488
|
+
kind: "watch",
|
|
489
|
+
pin,
|
|
490
|
+
body: callbackBodyText(call.arguments[1], source),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const elementMethod = readTreeElementMethod(call);
|
|
495
|
+
if (!elementMethod)
|
|
496
|
+
continue;
|
|
497
|
+
const nodeIndex = resolveNode(elementMethod.treeName, elementMethod.elemId);
|
|
498
|
+
if (nodeIndex === undefined)
|
|
499
|
+
continue;
|
|
500
|
+
if (elementMethod.method === "onClick" || elementMethod.method === "onHold" || elementMethod.method === "onRelease") {
|
|
501
|
+
callbacks.push({
|
|
502
|
+
nodeId: elementMethod.elemId,
|
|
503
|
+
nodeIndex,
|
|
504
|
+
kind: elementMethod.method === "onClick" ? "click" : elementMethod.method === "onHold" ? "hold" : "release",
|
|
505
|
+
body: callbackBodyText(call.arguments[0], source),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
else if (elementMethod.method === "onToggle") {
|
|
509
|
+
const pin = numericArgText(call.arguments[0], source, "0");
|
|
510
|
+
pinControls.push({
|
|
511
|
+
label: `${elementMethod.elemId}.onToggle(${pin})`,
|
|
512
|
+
kind: "toggle",
|
|
513
|
+
pin,
|
|
514
|
+
nodeId: elementMethod.elemId,
|
|
515
|
+
nodeIndex,
|
|
516
|
+
body: callbackBodyText(call.arguments[1], source),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
else if (elementMethod.method === "onChange") {
|
|
520
|
+
const nodeInfo = programNodes[nodeIndex];
|
|
521
|
+
if (nodeInfo?.tag === "input" || nodeInfo?.kind === "input") {
|
|
522
|
+
callbacks.push({
|
|
523
|
+
nodeId: elementMethod.elemId,
|
|
524
|
+
nodeIndex,
|
|
525
|
+
kind: "change",
|
|
526
|
+
body: callbackBodyText(call.arguments[0], source),
|
|
527
|
+
});
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const pin = numericArgText(call.arguments[0], source, "0");
|
|
531
|
+
const count = call.arguments[1] && ts.isNumericLiteral(call.arguments[1])
|
|
532
|
+
? Number(call.arguments[1].text)
|
|
533
|
+
: Math.max(programNodes[nodeIndex].options?.length ?? 0, 2);
|
|
534
|
+
pinControls.push({
|
|
535
|
+
label: `${elementMethod.elemId}.onChange(${pin})`,
|
|
536
|
+
kind: "change",
|
|
537
|
+
pin,
|
|
538
|
+
nodeId: elementMethod.elemId,
|
|
539
|
+
nodeIndex,
|
|
540
|
+
optionCount: count,
|
|
541
|
+
body: callbackBodyText(call.arguments[2], source),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else if (elementMethod.method === "onPress" || elementMethod.method === "onRelease") {
|
|
545
|
+
const pin = numericArgText(call.arguments[0], source, "0");
|
|
546
|
+
pinControls.push({
|
|
547
|
+
label: `${elementMethod.elemId}.${elementMethod.method}(${pin})`,
|
|
548
|
+
kind: elementMethod.method === "onPress" ? "press" : "release",
|
|
549
|
+
pin,
|
|
550
|
+
nodeId: elementMethod.elemId,
|
|
551
|
+
nodeIndex,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return { bindings, listBindings, callbacks, initialAssignments, intervals, pinControls, canvasBindings, moduleVars, diagnostics };
|
|
556
|
+
}
|
|
557
|
+
export async function buildPreviewSnapshot(options) {
|
|
558
|
+
const { config, projectRoot } = options;
|
|
559
|
+
const diagnostics = [];
|
|
560
|
+
const configDir = path.dirname(config.configPath);
|
|
561
|
+
if (!config.entry) {
|
|
562
|
+
throw new Error(`cuttlefish preview requires an entry field in ${config.configPath}`);
|
|
563
|
+
}
|
|
564
|
+
const entryFile = path.resolve(configDir, config.entry);
|
|
565
|
+
const entrySource = fs.readFileSync(entryFile, "utf-8");
|
|
566
|
+
const entryDir = path.dirname(entryFile);
|
|
567
|
+
const entryExt = path.extname(entryFile).toLowerCase();
|
|
568
|
+
let sourceFile;
|
|
569
|
+
let htmlText;
|
|
570
|
+
let cssText;
|
|
571
|
+
let htmlFilePath; // the effective .ui.html path (real or synthetic)
|
|
572
|
+
let uiImports;
|
|
573
|
+
if (entryExt === ".ui") {
|
|
574
|
+
// .ui single-file component: split into script/style/template.
|
|
575
|
+
const parts = splitUiFile(entrySource);
|
|
576
|
+
const baseName = path.basename(entryFile, ".ui");
|
|
577
|
+
htmlFilePath = entryFile + ".html"; // synthetic .ui.html path
|
|
578
|
+
// Inject the implicit screen import so the script's `screen` reference
|
|
579
|
+
// resolves during preview-expression evaluation.
|
|
580
|
+
const scriptWithImport = `import { screen } from './${baseName}.ui.html';\n` + parts.script;
|
|
581
|
+
sourceFile = ts.createSourceFile(entryFile, scriptWithImport, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
582
|
+
htmlText = parts.html;
|
|
583
|
+
cssText = parts.style;
|
|
584
|
+
uiImports = [{ treeName: "screen", htmlPath: htmlFilePath }];
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
sourceFile = ts.createSourceFile(entryFile, entrySource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
588
|
+
uiImports = findUIModuleImports(sourceFile, entryDir, diagnostics);
|
|
589
|
+
const firstImport = uiImports[0];
|
|
590
|
+
if (!firstImport) {
|
|
591
|
+
throw new Error(`Preview could not find a .ui.html import in ${entryFile}`);
|
|
592
|
+
}
|
|
593
|
+
if (uiImports.length > 1) {
|
|
594
|
+
diagnostics.push({
|
|
595
|
+
severity: "warning",
|
|
596
|
+
message: "Preview v1 renders the first imported UI tree only.",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
htmlFilePath = firstImport.htmlPath;
|
|
600
|
+
htmlText = fs.readFileSync(firstImport.htmlPath, "utf-8");
|
|
601
|
+
const cssPath = themeCssPath(config.display?.themeCss, firstImport.htmlPath, configDir);
|
|
602
|
+
cssText = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf-8") : "";
|
|
603
|
+
}
|
|
604
|
+
const registry = await loadProfileRegistry(config.framework);
|
|
605
|
+
const resolved = resolveDisplayProfile(config.display ?? { profile: "ili9341-spi" }, registry);
|
|
606
|
+
const profile = resolved.profile;
|
|
607
|
+
const displaySize = effectiveDisplaySize(profile);
|
|
608
|
+
const parsedHtml = parseHtmlWithKeyboards(htmlText);
|
|
609
|
+
const fullCss = cssText + "\n" + extractStyleBlocks(htmlText);
|
|
610
|
+
const cssRules = (() => {
|
|
611
|
+
const previousThemeClass = getThemeClass();
|
|
612
|
+
setThemeClass(config.display?.themeClass ?? null);
|
|
613
|
+
try {
|
|
614
|
+
return parseCss(fullCss);
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
setThemeClass(previousThemeClass);
|
|
618
|
+
}
|
|
619
|
+
})();
|
|
620
|
+
const fontFaces = parseFontFaces(fullCss);
|
|
621
|
+
const rawKeyframes = parseKeyframes(fullCss);
|
|
622
|
+
const styled = resolveStyles(parsedHtml.tree, cssRules);
|
|
623
|
+
const allStyledScreens = parsedHtml.screens.map((screen) => resolveStyles(screen, cssRules));
|
|
624
|
+
const fontRoot = { tag: "screen", classes: [], style: {}, children: allStyledScreens };
|
|
625
|
+
const fontAssets = buildUIFontAssets(fontRoot, fontFaces, path.dirname(htmlFilePath));
|
|
626
|
+
const viewport = { x: 0, y: 0, w: displaySize.width, h: displaySize.height };
|
|
627
|
+
const boxes = allStyledScreens.flatMap((screen) => {
|
|
628
|
+
const engine = selectEngine(screen);
|
|
629
|
+
return engine.arrange(screen, viewport, measureWithFonts(fontAssets));
|
|
630
|
+
});
|
|
631
|
+
const keyframeSets = buildKeyframeSets(rawKeyframes, profile.colorFormat);
|
|
632
|
+
const imageAssets = loadImageAssets(allStyledScreens.length > 0 ? allStyledScreens : [styled], path.dirname(htmlFilePath));
|
|
633
|
+
const program = lowerUIToModel(styled, boxes, profile.colorFormat, profile, fontAssets, allStyledScreens, imageAssets.nodeIdToAssetIndex, keyframeSets, imageAssets.assets);
|
|
634
|
+
const specs = extractAuthorSpecs(sourceFile, uiImports, program.nodes);
|
|
635
|
+
const hrefCallbacks = collectHrefCallbacks(allStyledScreens, program.nodes);
|
|
636
|
+
// {expr} interpolation bindings synthesized from the HTML (mirrors the runtime's
|
|
637
|
+
// auto-wire synthesis). Authors write `taps: {count}` in markup; this collects
|
|
638
|
+
// them so the preview evaluates the template-literal expression each frame.
|
|
639
|
+
const interpolationBindings = collectInterpolationBindings(allStyledScreens.length > 0 ? allStyledScreens : [styled], program.nodes);
|
|
640
|
+
// on:* declarative event handlers from markup → preview callbacks (named fn).
|
|
641
|
+
const eventCallbacks = collectEventCallbacks(allStyledScreens.length > 0 ? allStyledScreens : [styled], program.nodes, collectNamedFunctionBodies(sourceFile));
|
|
642
|
+
// bind:* declarative two-way bindings: read (signal → node) + write-back
|
|
643
|
+
// (node → signal via change callbacks).
|
|
644
|
+
const bindResult = collectBindBindings(allStyledScreens.length > 0 ? allStyledScreens : [styled], program.nodes);
|
|
645
|
+
return {
|
|
646
|
+
projectRoot,
|
|
647
|
+
entryFile,
|
|
648
|
+
htmlFile: htmlFilePath,
|
|
649
|
+
profileName: typeof config.display?.profile === "string" ? config.display.profile : undefined,
|
|
650
|
+
program,
|
|
651
|
+
keyboardTemplates: parsedHtml.keyboards,
|
|
652
|
+
cssRules,
|
|
653
|
+
uiTreeNames: [...new Set(uiImports.map((imp) => imp.treeName))],
|
|
654
|
+
font: loadFont(projectRoot, diagnostics),
|
|
655
|
+
bindings: [...specs.bindings, ...interpolationBindings, ...bindResult.bindings],
|
|
656
|
+
listBindings: specs.listBindings,
|
|
657
|
+
callbacks: [...hrefCallbacks, ...specs.callbacks, ...eventCallbacks, ...bindResult.callbacks],
|
|
658
|
+
initialAssignments: specs.initialAssignments,
|
|
659
|
+
intervals: specs.intervals,
|
|
660
|
+
pinControls: specs.pinControls,
|
|
661
|
+
canvasBindings: specs.canvasBindings,
|
|
662
|
+
moduleVars: specs.moduleVars,
|
|
663
|
+
diagnostics: [...diagnostics, ...specs.diagnostics],
|
|
664
|
+
};
|
|
665
|
+
}
|