@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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1443 -0
  3. package/dist/engine-index.d.ts +7 -0
  4. package/dist/engine-index.js +41 -0
  5. package/dist/index.d.ts +165 -0
  6. package/dist/index.js +42 -0
  7. package/dist/preview/build-program.d.ts +7 -0
  8. package/dist/preview/build-program.js +665 -0
  9. package/dist/preview/host-gfx.d.ts +90 -0
  10. package/dist/preview/host-gfx.js +552 -0
  11. package/dist/preview/host-ui-runtime.d.ts +241 -0
  12. package/dist/preview/host-ui-runtime.js +3236 -0
  13. package/dist/preview/types.d.ts +83 -0
  14. package/dist/preview/types.js +1 -0
  15. package/dist/types.d.ts +108 -0
  16. package/dist/types.js +10 -0
  17. package/dist/ui-engine/block-layout.d.ts +8 -0
  18. package/dist/ui-engine/block-layout.js +95 -0
  19. package/dist/ui-engine/color.d.ts +49 -0
  20. package/dist/ui-engine/color.js +273 -0
  21. package/dist/ui-engine/css-parser.d.ts +155 -0
  22. package/dist/ui-engine/css-parser.js +1058 -0
  23. package/dist/ui-engine/default-keyboards.d.ts +3 -0
  24. package/dist/ui-engine/default-keyboards.js +53 -0
  25. package/dist/ui-engine/easing.d.ts +12 -0
  26. package/dist/ui-engine/easing.js +76 -0
  27. package/dist/ui-engine/font-assets.d.ts +50 -0
  28. package/dist/ui-engine/font-assets.js +431 -0
  29. package/dist/ui-engine/html-parser.d.ts +116 -0
  30. package/dist/ui-engine/html-parser.js +284 -0
  31. package/dist/ui-engine/image-assets.d.ts +21 -0
  32. package/dist/ui-engine/image-assets.js +125 -0
  33. package/dist/ui-engine/inline-parser.d.ts +30 -0
  34. package/dist/ui-engine/inline-parser.js +66 -0
  35. package/dist/ui-engine/keyframes.d.ts +3 -0
  36. package/dist/ui-engine/keyframes.js +39 -0
  37. package/dist/ui-engine/layout-engine.d.ts +35 -0
  38. package/dist/ui-engine/layout-engine.js +203 -0
  39. package/dist/ui-engine/model.d.ts +239 -0
  40. package/dist/ui-engine/model.js +987 -0
  41. package/dist/ui-engine/rich-layout.d.ts +51 -0
  42. package/dist/ui-engine/rich-layout.js +220 -0
  43. package/dist/ui-engine/run-types.d.ts +13 -0
  44. package/dist/ui-engine/run-types.js +1 -0
  45. package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
  46. package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
  47. package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
  48. package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
  49. package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
  50. package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
  51. package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
  52. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
  53. package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
  54. package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
  55. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
  56. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
  57. package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
  58. package/dist/ui-engine/runtime-header/display-shim.js +104 -0
  59. package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
  60. package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
  61. package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
  62. package/dist/ui-engine/runtime-header/guard-close.js +8 -0
  63. package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
  64. package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
  65. package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
  66. package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
  67. package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
  68. package/dist/ui-engine/runtime-header/keyboard.js +342 -0
  69. package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
  70. package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
  71. package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
  72. package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
  73. package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
  74. package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
  75. package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
  76. package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
  77. package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
  78. package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
  79. package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
  80. package/dist/ui-engine/runtime-header/structs.js +220 -0
  81. package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
  82. package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
  83. package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
  84. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
  85. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
  86. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
  87. package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
  88. package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
  89. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
  90. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
  91. package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
  92. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
  93. package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
  94. package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
  95. package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
  96. package/dist/ui-engine/runtime-header/tick.js +17 -0
  97. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
  98. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
  99. package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
  100. package/dist/ui-engine/runtime-header/types-defines.js +79 -0
  101. package/dist/ui-engine/runtime-header.d.ts +1 -0
  102. package/dist/ui-engine/runtime-header.js +69 -0
  103. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
  104. package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
  105. package/dist/ui-engine/select-engine.d.ts +4 -0
  106. package/dist/ui-engine/select-engine.js +19 -0
  107. package/dist/ui-engine/style-resolver.d.ts +77 -0
  108. package/dist/ui-engine/style-resolver.js +341 -0
  109. package/dist/ui-engine/text-layout.d.ts +19 -0
  110. package/dist/ui-engine/text-layout.js +92 -0
  111. package/dist/ui-engine/transpile-ui.d.ts +11 -0
  112. package/dist/ui-engine/transpile-ui.js +33 -0
  113. package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
  114. package/dist/ui-engine/ua-stylesheet.js +56 -0
  115. package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
  116. package/dist/ui-engine/ui-file-splitter.js +47 -0
  117. package/dist/ui-engine/ui-lowering.d.ts +33 -0
  118. package/dist/ui-engine/ui-lowering.js +390 -0
  119. package/dist/ui-engine/ui-registry.d.ts +81 -0
  120. package/dist/ui-engine/ui-registry.js +429 -0
  121. package/dist/ui-engine/yoga-layout.d.ts +12 -0
  122. package/dist/ui-engine/yoga-layout.js +314 -0
  123. package/package.json +86 -0
  124. package/src/engine-index.ts +51 -0
  125. package/src/index.ts +232 -0
  126. package/src/preview/build-program.ts +734 -0
  127. package/src/preview/host-gfx.ts +580 -0
  128. package/src/preview/host-ui-runtime.ts +3245 -0
  129. package/src/preview/types.ts +96 -0
  130. package/src/types.ts +127 -0
  131. package/src/ui-engine/block-layout.ts +104 -0
  132. package/src/ui-engine/color.ts +263 -0
  133. package/src/ui-engine/css-parser.ts +1003 -0
  134. package/src/ui-engine/default-keyboards.ts +58 -0
  135. package/src/ui-engine/easing.ts +53 -0
  136. package/src/ui-engine/font-assets.ts +512 -0
  137. package/src/ui-engine/html-parser.ts +397 -0
  138. package/src/ui-engine/image-assets.ts +170 -0
  139. package/src/ui-engine/inline-parser.ts +78 -0
  140. package/src/ui-engine/keyframes.ts +47 -0
  141. package/src/ui-engine/layout-engine.ts +228 -0
  142. package/src/ui-engine/model.ts +1225 -0
  143. package/src/ui-engine/rich-layout.ts +257 -0
  144. package/src/ui-engine/run-types.ts +14 -0
  145. package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
  146. package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
  147. package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
  148. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
  149. package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
  150. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
  151. package/src/ui-engine/runtime-header/display-shim.ts +104 -0
  152. package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
  153. package/src/ui-engine/runtime-header/guard-close.ts +8 -0
  154. package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
  155. package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
  156. package/src/ui-engine/runtime-header/keyboard.ts +342 -0
  157. package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
  158. package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
  159. package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
  160. package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
  161. package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
  162. package/src/ui-engine/runtime-header/structs.ts +220 -0
  163. package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
  164. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
  165. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
  166. package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
  167. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
  168. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
  169. package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
  170. package/src/ui-engine/runtime-header/tick.ts +18 -0
  171. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
  172. package/src/ui-engine/runtime-header/types-defines.ts +79 -0
  173. package/src/ui-engine/runtime-header.ts +71 -0
  174. package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
  175. package/src/ui-engine/select-engine.ts +21 -0
  176. package/src/ui-engine/style-resolver.ts +416 -0
  177. package/src/ui-engine/text-layout.ts +114 -0
  178. package/src/ui-engine/transpile-ui.ts +45 -0
  179. package/src/ui-engine/ua-stylesheet.ts +60 -0
  180. package/src/ui-engine/ui-file-splitter.ts +59 -0
  181. package/src/ui-engine/ui-lowering.ts +459 -0
  182. package/src/ui-engine/ui-registry.ts +530 -0
  183. package/src/ui-engine/yoga-layout.ts +309 -0
@@ -0,0 +1,116 @@
1
+ import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
2
+ import { InlineItem } from "./inline-parser.js";
3
+ export interface UIElementNode {
4
+ tag: string;
5
+ /** Original HTML tag before remapping (label/a/div/...), so CSS tag
6
+ * selectors still match remapped elements. Equals tag when no remap. */
7
+ origTag?: string;
8
+ id?: string;
9
+ classes: string[];
10
+ text?: string;
11
+ /** Value attribute (for <option>, <radio>). */
12
+ value?: string;
13
+ /** Name attribute (for <radio>: groups radios together). */
14
+ name?: string;
15
+ /** Checked attribute (for <radio>: initially selected). */
16
+ checked?: boolean;
17
+ /** Min/max attributes (for <range>). */
18
+ min?: string;
19
+ max?: string;
20
+ /** Input type (for <input>: "text" | "number"). */
21
+ type?: "text" | "number";
22
+ /** Placeholder (for <input>). */
23
+ placeholder?: string;
24
+ /** Max length (for <input>). */
25
+ maxlength?: number;
26
+ /** Keyboard ref id (for <input>). */
27
+ keyboard?: string;
28
+ /** HTML hidden attribute: removes the element subtree from layout/rendering. */
29
+ hidden?: boolean;
30
+ /** Inline style attribute: style="color: red; font-size: 16px" */
31
+ inlineStyle?: string;
32
+ /** Navigation target for <a href="#screenId"> links. */
33
+ href?: string;
34
+ /** Image source path for <img src="...">. */
35
+ src?: string;
36
+ /** Image width in pixels (for <img>). */
37
+ imgWidth?: number;
38
+ /** Image height in pixels (for <img>). */
39
+ imgHeight?: number;
40
+ /** Item height in pixels (for <list item-height="24">). */
41
+ itemHeight?: number;
42
+ /** Canvas buffer width in pixels (for <canvas>). */
43
+ canvasW?: number;
44
+ /** Canvas buffer height in pixels (for <canvas>). */
45
+ canvasH?: number;
46
+ /** Disabled state */
47
+ disabled?: boolean;
48
+ children: UIElementNode[];
49
+ /** Ordered inline content sequence (text/element/break items). Present only
50
+ * for text nodes with mixed inline children; absent for plain-text nodes. */
51
+ inline?: InlineItem[];
52
+ /** True when the text content contains a `{expr}` interpolation, which the
53
+ * auto-wire layer lowers to an implicit ui.bind(node,'text',...) text
54
+ * binding. Plain text (no braces) is unchanged. */
55
+ hasInterpolation?: boolean;
56
+ /** Declarative event handlers from on:* attributes (e.g. on:click="save").
57
+ * Keys: click | hold | release | change. Values: a named TS export function
58
+ * the transpiler emits as a standalone C++ function; the handler table
59
+ * references it by name. Absent when no on:* attributes are present. */
60
+ events?: {
61
+ click?: string;
62
+ hold?: string;
63
+ release?: string;
64
+ change?: string;
65
+ };
66
+ /** Declarative two-way bindings from bind:* attributes (e.g. bind:text="ssid").
67
+ * Keys: text | value. Values: a signal name — the node reflects the signal
68
+ * (one-way: signal → node), and user input writes back (node → signal.set).
69
+ * Absent when no bind:* attributes are present. */
70
+ bind?: {
71
+ text?: string;
72
+ value?: string;
73
+ };
74
+ /** TS handle name (screen.<ref>), separate from the CSS #id selector target.
75
+ * When absent, falls back to `id` (backward-compatible). Lets an author keep
76
+ * a CSS id without leaking every styled element into the TS surface. */
77
+ ref?: string;
78
+ /** For <select>: parsed option list from <option> children. */
79
+ options?: Array<{
80
+ value: string;
81
+ text: string;
82
+ }>;
83
+ }
84
+ /** A single key in a keyboard template. */
85
+ export interface UIKeyTemplate {
86
+ /** Character to insert, or label for special keys. */
87
+ ch: string;
88
+ /** 0=char, 1=shift, 2=backspace, 3=ok, 4=page-swap. */
89
+ special: 0 | 1 | 2 | 3 | 4;
90
+ /** CSS classes from <key class="..."> for styling. */
91
+ classes?: string[];
92
+ }
93
+ /** A keyboard template parsed from <keyboard>. */
94
+ export interface KeyboardTemplate {
95
+ id: string;
96
+ variant: "alpha" | "number";
97
+ rows: UIKeyTemplate[][];
98
+ /** CSS classes from <keyboard class="..."> for styling the keyboard background. */
99
+ classes?: string[];
100
+ }
101
+ export interface ParsedHtml {
102
+ /** The first <screen> tree (backward compat). */
103
+ tree: UIElementNode;
104
+ /** All <screen> roots (for multi-screen navigation). */
105
+ screens: UIElementNode[];
106
+ keyboards: KeyboardTemplate[];
107
+ }
108
+ /** Extract <style>...</style> block contents from HTML source.
109
+ * Returns the concatenated CSS text (empty if no style blocks). */
110
+ export declare function extractStyleBlocks(src: string): string;
111
+ export declare function parseHtml(src: string, diagnostics?: Diagnostic[]): UIElementNode;
112
+ /** Parse all <screen> roots from HTML. Returns one tree per screen.
113
+ * Used for multi-screen navigation (<a href="#screenId">). */
114
+ export declare function parseAllScreens(src: string, diagnostics?: Diagnostic[]): UIElementNode[];
115
+ /** Parse HTML, returning both the <screen> tree and any <keyboard> templates. */
116
+ export declare function parseHtmlWithKeyboards(src: string, diagnostics?: Diagnostic[]): ParsedHtml;
@@ -0,0 +1,284 @@
1
+ // ---------------------------------------------------------------------------
2
+ // HTML parser — uses linkedom for robust DOM parsing, then adapts to the
3
+ // UIElementNode shape that the rest of the pipeline expects.
4
+ //
5
+ // Supported subset:
6
+ // - One <screen> root (required, exactly one).
7
+ // - Child elements: <text>, <button>, <view> (a generic container).
8
+ // - Attributes: id="...", class="a b".
9
+ // - Text content of leaf elements.
10
+ //
11
+ // The public API (parseHtml: string → UIElementNode) is unchanged — callers
12
+ // don't know whether linkedom or a regex parser is behind it.
13
+ // ---------------------------------------------------------------------------
14
+ import { parseHTML } from "linkedom";
15
+ import { collectInlineSequence, INLINE_TAGS } from "./inline-parser.js";
16
+ const SUPPORTED_TAGS = new Set(["screen", "text", "button", "view", "check", "select", "option", "label", "radio", "progress", "range", "input", "keyboard", "row", "key", "style", "a", "img", "list", "canvas", "br"]);
17
+ /** HTML tag aliases — common HTML elements remapped to internal primitives.
18
+ * Semantic block containers -> view; inline/heading text tags -> text.
19
+ * Applied before the SUPPORTED_TAGS check so authors can write familiar HTML. */
20
+ const TAG_REMAP = {
21
+ // Block-level containers -> view (flexbox/positioning surface)
22
+ body: "view", div: "view", header: "view", footer: "view", nav: "view",
23
+ main: "view", section: "view", article: "view", aside: "view",
24
+ // Inline/heading text -> text
25
+ span: "text", p: "text",
26
+ h1: "text", h2: "text", h3: "text", h4: "text", h5: "text", h6: "text",
27
+ // Styling tags -> text (inline; resolver applies bold/italic/underline defaults
28
+ // and absorbs them into the parent's run list).
29
+ b: "text", strong: "text", i: "text", em: "text", u: "text",
30
+ };
31
+ /** Extract <style>...</style> block contents from HTML source.
32
+ * Returns the concatenated CSS text (empty if no style blocks). */
33
+ export function extractStyleBlocks(src) {
34
+ const matches = src.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi);
35
+ return Array.from(matches).map(m => m[1]).join("\n");
36
+ }
37
+ export function parseHtml(src, diagnostics) {
38
+ return parseAllScreens(src, diagnostics)[0];
39
+ }
40
+ /** Parse all <screen> roots from HTML. Returns one tree per screen.
41
+ * Used for multi-screen navigation (<a href="#screenId">). */
42
+ export function parseAllScreens(src, diagnostics) {
43
+ const withoutComments = src.replace(/<!--[\s\S]*?-->/g, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
44
+ const wrapped = `<div id="__root__">${withoutComments}</div>`;
45
+ const { document } = parseHTML(wrapped);
46
+ const root = document.getElementById("__root__");
47
+ if (!root) {
48
+ throw new Error("UI HTML: failed to parse document");
49
+ }
50
+ const screenEls = Array.from(root.children).filter((c) => c.tagName.toLowerCase() === "screen");
51
+ if (screenEls.length === 0) {
52
+ throw new Error("UI HTML must have at least one <screen> root element");
53
+ }
54
+ return screenEls.map(el => domToUIElementNode(el, diagnostics));
55
+ }
56
+ /** Parse HTML, returning both the <screen> tree and any <keyboard> templates. */
57
+ export function parseHtmlWithKeyboards(src, diagnostics) {
58
+ const withoutComments = src.replace(/<!--[\s\S]*?-->/g, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
59
+ const wrapped = `<div id="__root__">${withoutComments}</div>`;
60
+ const { document } = parseHTML(wrapped);
61
+ const root = document.getElementById("__root__");
62
+ if (!root) {
63
+ throw new Error("UI HTML: failed to parse document");
64
+ }
65
+ // Parse keyboards first (they are siblings of <screen>, not children).
66
+ const keyboards = [];
67
+ for (const child of Array.from(root.children)) {
68
+ if (child.tagName.toLowerCase() !== "keyboard")
69
+ continue;
70
+ keyboards.push(parseKeyboardElement(child));
71
+ }
72
+ // Parse all screens (for multi-screen navigation) + keyboards.
73
+ const screens = parseAllScreens(src, diagnostics);
74
+ const tree = screens[0];
75
+ return { tree, screens, keyboards };
76
+ }
77
+ /** Parse a <keyboard> element into a KeyboardTemplate. */
78
+ function parseKeyboardElement(el) {
79
+ const id = el.getAttribute("id") || "";
80
+ const variantAttr = el.getAttribute("variant");
81
+ const variant = variantAttr === "number" ? "number" : "alpha";
82
+ const classAttr = el.getAttribute("class") || "";
83
+ const classes = classAttr.split(/\s+/).filter(Boolean);
84
+ const rows = [];
85
+ for (const rowEl of Array.from(el.children)) {
86
+ if (rowEl.tagName.toLowerCase() !== "row")
87
+ continue;
88
+ const row = [];
89
+ for (const keyEl of Array.from(rowEl.children)) {
90
+ if (keyEl.tagName.toLowerCase() !== "key")
91
+ continue;
92
+ row.push(parseKeyElement(keyEl));
93
+ }
94
+ if (row.length > 0)
95
+ rows.push(row);
96
+ }
97
+ return { id, variant, rows, classes: classes.length > 0 ? classes : undefined };
98
+ }
99
+ /** Parse a <key> element. Special keys are identified by label or special attr. */
100
+ function parseKeyElement(el) {
101
+ const label = el.textContent?.trim() || "";
102
+ const classAttr = el.getAttribute("class") || "";
103
+ const classes = classAttr.split(/\s+/).filter(Boolean);
104
+ const specialAttr = el.getAttribute("special");
105
+ let special = 0;
106
+ if (specialAttr !== null) {
107
+ const s = parseInt(specialAttr, 10);
108
+ if (s >= 1 && s <= 4)
109
+ special = s;
110
+ }
111
+ else {
112
+ // Recognize special keys by conventional labels.
113
+ if (label === "⇧" || label.toUpperCase() === "SHIFT")
114
+ special = 1;
115
+ else if (label === "⌫" || label.toUpperCase() === "BACKSPACE")
116
+ special = 2;
117
+ else if (label.toUpperCase() === "OK")
118
+ special = 3;
119
+ else if (label === "123" || label.toUpperCase() === "ABC")
120
+ special = 4;
121
+ }
122
+ return { ch: label, special, classes: classes.length > 0 ? classes : undefined };
123
+ }
124
+ /** Adapt a DOM element to UIElementNode, recursively walking children. */
125
+ function domToUIElementNode(el, diagnostics) {
126
+ const tag = el.tagName.toLowerCase();
127
+ // <label> and <a> are treated as <text> internally; HTML aliases
128
+ // (div/header/span/p/h1-h6/...) remap to view or text.
129
+ const remapped = TAG_REMAP[tag];
130
+ const effectiveTag = remapped ? remapped
131
+ : (tag === "label" || tag === "a") ? "text" : tag;
132
+ if (!SUPPORTED_TAGS.has(effectiveTag)) {
133
+ throw new Error(`Unsupported tag <${tag}> — supported: ${[...SUPPORTED_TAGS].join(", ")}`);
134
+ }
135
+ const id = el.getAttribute("id") || undefined;
136
+ const refAttr = el.getAttribute("ref") || undefined;
137
+ const classAttr = el.getAttribute("class") || "";
138
+ const classes = classAttr.split(/\s+/).filter(Boolean);
139
+ const valueAttr = el.getAttribute("value") || undefined;
140
+ const nameAttr = el.getAttribute("name") || undefined;
141
+ const checkedAttr = el.hasAttribute("checked");
142
+ const minAttr = el.getAttribute("min") || undefined;
143
+ const maxAttr = el.getAttribute("max") || undefined;
144
+ const typeAttr = tag === "input"
145
+ ? (el.getAttribute("type") === "number" ? "number" : "text")
146
+ : undefined;
147
+ const placeholderAttr = el.getAttribute("placeholder") || undefined;
148
+ const maxlengthAttr = el.getAttribute("maxlength");
149
+ // <input> defaults maxlength to 16 when absent or unparseable.
150
+ const maxlengthNum = tag === "input"
151
+ ? (maxlengthAttr ? (parseInt(maxlengthAttr, 10) || 16) : 16)
152
+ : undefined;
153
+ const keyboardAttr = el.getAttribute("keyboard") || undefined;
154
+ const hiddenAttr = el.hasAttribute("hidden");
155
+ const inlineStyleAttr = el.getAttribute("style") || undefined;
156
+ const hrefAttr = (tag === "a" || tag === "button") ? (el.getAttribute("href") || undefined) : undefined;
157
+ // Directive value normalization: Svelte encloses directive values in braces
158
+ // (on:click={handler}, bind:value={signal}). The bare-string form
159
+ // (on:click="handler") is also accepted for backward compatibility. Strip the
160
+ // surrounding braces when present so both forms produce the same value.
161
+ const normalizeDirective = (v) => {
162
+ if (!v)
163
+ return undefined;
164
+ const trimmed = v.trim();
165
+ if (!trimmed)
166
+ return undefined;
167
+ // Svelte brace form: {expr} → expr
168
+ const braceMatch = /^\{(.+)\}$/.exec(trimmed);
169
+ return braceMatch ? braceMatch[1].trim() : trimmed;
170
+ };
171
+ // Declarative on:* event attributes → named-function references. Accept both
172
+ // Svelte form (on:click={fn}) and quoted form (on:click="fn").
173
+ const EVENT_KINDS = ["click", "hold", "release", "change"];
174
+ const events = {};
175
+ for (const kind of EVENT_KINDS) {
176
+ const v = normalizeDirective(el.getAttribute(`on:${kind}`));
177
+ if (v)
178
+ events[kind] = v;
179
+ }
180
+ const hasEvents = Object.keys(events).length > 0;
181
+ // Declarative bind:* two-way bindings → signal names. Accept both
182
+ // Svelte form (bind:text={signal}) and quoted form (bind:text="signal").
183
+ const BIND_KINDS = ["text", "value"];
184
+ const bind = {};
185
+ for (const kind of BIND_KINDS) {
186
+ const v = normalizeDirective(el.getAttribute(`bind:${kind}`));
187
+ if (v)
188
+ bind[kind] = v;
189
+ }
190
+ const hasBind = Object.keys(bind).length > 0;
191
+ const srcAttr = tag === "img" ? (el.getAttribute("src") || undefined) : undefined;
192
+ const imgWidthAttr = tag === "img" ? parseInt(el.getAttribute("width") || "0", 10) : undefined;
193
+ const imgHeightAttr = tag === "img" ? parseInt(el.getAttribute("height") || "0", 10) : undefined;
194
+ const itemHeightAttr = tag === "list" ? (parseInt(el.getAttribute("item-height") || "24", 10) || 24) : undefined;
195
+ const canvasWAttr = tag === "canvas" ? (parseInt(el.getAttribute("width") || "0", 10) || 0) : undefined;
196
+ const canvasHAttr = tag === "canvas" ? (parseInt(el.getAttribute("height") || "0", 10) || 0) : undefined;
197
+ const disabledAttr = el.hasAttribute("disabled");
198
+ // For <select>, parse <option> children into an options list
199
+ if (tag === "select") {
200
+ const optionEls = Array.from(el.children).filter(c => c.tagName.toLowerCase() === "option");
201
+ if (optionEls.length > 0) {
202
+ const options = optionEls.map(opt => ({
203
+ value: opt.getAttribute("value") || opt.textContent?.trim() || "",
204
+ text: opt.textContent?.trim() || "",
205
+ }));
206
+ // Use the first option's text as the initial display text
207
+ const firstText = options[0]?.text ?? "";
208
+ return {
209
+ tag: "select",
210
+ id, classes, text: firstText, value: valueAttr,
211
+ hidden: hiddenAttr,
212
+ inlineStyle: inlineStyleAttr,
213
+ children: [],
214
+ options,
215
+ };
216
+ }
217
+ // Fallback: comma-separated text (legacy shorthand)
218
+ const text = el.textContent?.trim() || "";
219
+ const optNames = text.split(",").map(s => s.trim()).filter(Boolean);
220
+ return {
221
+ tag: "select",
222
+ id, classes, text: optNames[0] || "",
223
+ hidden: hiddenAttr,
224
+ inlineStyle: inlineStyleAttr,
225
+ children: [],
226
+ options: optNames.map(t => ({ value: t.toLowerCase(), text: t })),
227
+ };
228
+ }
229
+ // Text content: only direct text, not children's text.
230
+ let text;
231
+ const childElements = Array.from(el.children).filter((c) => {
232
+ const ct = c.tagName.toLowerCase();
233
+ // Accept native tags and HTML aliases (div/span/p/h1-h6/...) that remap later.
234
+ const accepted = SUPPORTED_TAGS.has(ct) || TAG_REMAP[ct] !== undefined;
235
+ if (!accepted && ct !== "option" && ct !== "br" && diagnostics) {
236
+ diagnostics.push({
237
+ severity: "warning",
238
+ message: `Unknown HTML tag <${ct}> — ignored.`,
239
+ hint: `Supported tags: ${[...SUPPORTED_TAGS].sort().join(", ")}.`,
240
+ code: "unknown-html-tag",
241
+ source: ct,
242
+ });
243
+ }
244
+ return accepted && ct !== "option" && ct !== "br";
245
+ });
246
+ // Inline-bearing text nodes collect an ordered inline sequence instead of a
247
+ // single text string. Only effective-tag "text" can be inline-bearing;
248
+ // collectInlineSequence returns undefined for plain text or block-child nodes.
249
+ let inline;
250
+ if (effectiveTag === "text") {
251
+ inline = collectInlineSequence(el, diagnostics ?? []);
252
+ }
253
+ // Text content: only direct text, and only when there's no inline sequence
254
+ // (inline content is captured above; the plain-text path is unchanged).
255
+ let hasInterpolation = false;
256
+ if (!inline && childElements.length === 0) {
257
+ const parts = [];
258
+ for (const child of Array.from(el.childNodes)) {
259
+ if (child.nodeType === 3) {
260
+ parts.push(child.textContent ?? "");
261
+ continue;
262
+ }
263
+ const childTag = child.tagName?.toLowerCase();
264
+ if (childTag === "br")
265
+ parts.push("\n");
266
+ }
267
+ const tc = parts.join("").trim();
268
+ if (tc) {
269
+ text = tc;
270
+ // Detect a `{expr}` interpolation (non-empty content between braces).
271
+ // Inline + interpolation is rejected later by the run-text-binding guard.
272
+ hasInterpolation = /\{[^{}]+\}/.test(tc);
273
+ }
274
+ }
275
+ const remappedFrom = (remapped || tag === "label" || tag === "a") && tag !== effectiveTag ? tag : undefined;
276
+ const node = { tag: effectiveTag, origTag: remappedFrom, id, classes, text, value: valueAttr, name: nameAttr, checked: checkedAttr, min: minAttr, max: maxAttr, type: typeAttr, placeholder: placeholderAttr, maxlength: maxlengthNum, keyboard: keyboardAttr, hidden: hiddenAttr, inlineStyle: inlineStyleAttr, href: hrefAttr, src: srcAttr, imgWidth: imgWidthAttr || undefined, imgHeight: imgHeightAttr || undefined, itemHeight: itemHeightAttr, canvasW: canvasWAttr, canvasH: canvasHAttr, disabled: disabledAttr, inline, hasInterpolation, events: hasEvents ? events : undefined, bind: hasBind ? bind : undefined, ref: refAttr, children: [] };
277
+ for (const child of childElements) {
278
+ // Inline children are absorbed into `inline`; don't also emit them as nodes.
279
+ if (inline && INLINE_TAGS.has(child.tagName.toLowerCase()))
280
+ continue;
281
+ node.children.push(domToUIElementNode(child, diagnostics));
282
+ }
283
+ return node;
284
+ }
@@ -0,0 +1,21 @@
1
+ import type { StyledNode } from "./style-resolver.js";
2
+ type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
3
+ export interface UIImageAsset {
4
+ /** C++-safe unique id used for the generated data symbol. */
5
+ id: string;
6
+ /** Pixel width of the raw RGB565 asset. */
7
+ width: number;
8
+ /** Pixel height of the raw RGB565 asset. */
9
+ height: number;
10
+ /** RGB565 pixel data (width * height values, row-major). */
11
+ data: number[];
12
+ }
13
+ export interface LoadedUIImageAssets {
14
+ assets: UIImageAsset[];
15
+ nodeIdToAssetIndex: Map<string, number>;
16
+ }
17
+ export declare function loadImageAssets(roots: StyledNode | StyledNode[], htmlDir: string): LoadedUIImageAssets;
18
+ export declare function emitImageTables(assets: UIImageAsset[], colorFormat?: ColorFormat): string;
19
+ /** Read a raw RGB565 binary file and return a padded UIImageAsset. */
20
+ export declare function readRgb565Image(srcPath: string, htmlDir: string, id: string, width: number, height: number): UIImageAsset | null;
21
+ export {};
@@ -0,0 +1,125 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Image asset loader: reads raw RGB565 binary files for <img> nodes and keeps
3
+ // node ids mapped to the generated image table indices.
4
+ // ---------------------------------------------------------------------------
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ function sanitizeCppIdentifier(value, fallback) {
8
+ const sanitized = value.replace(/[^a-zA-Z0-9_]/g, "_");
9
+ const base = sanitized.length > 0 ? sanitized : fallback;
10
+ return /^[A-Za-z_]/.test(base) ? base : `img_${base}`;
11
+ }
12
+ function uniqueAssetId(preferred, index, used) {
13
+ const base = sanitizeCppIdentifier(preferred, `img_${index}`);
14
+ let candidate = base;
15
+ let suffix = 1;
16
+ while (used.has(candidate)) {
17
+ candidate = `${base}_${suffix++}`;
18
+ }
19
+ used.add(candidate);
20
+ return candidate;
21
+ }
22
+ function imageNaturalWidth(node) {
23
+ const width = node.imgWidth;
24
+ return width && width > 0 ? width : 32;
25
+ }
26
+ function imageNaturalHeight(node) {
27
+ const height = node.imgHeight;
28
+ return height && height > 0 ? height : 32;
29
+ }
30
+ function imageAssetKey(src, htmlDir, width, height) {
31
+ const abs = path.isAbsolute(src) ? src : path.resolve(htmlDir, src);
32
+ return `${abs}\0${width}\0${height}`;
33
+ }
34
+ export function loadImageAssets(roots, htmlDir) {
35
+ const assets = [];
36
+ const nodeIdToAssetIndex = new Map();
37
+ const keyToAssetIndex = new Map();
38
+ const usedIds = new Set();
39
+ const walk = (node) => {
40
+ const src = node.src;
41
+ if (src && node.id) {
42
+ const width = imageNaturalWidth(node);
43
+ const height = imageNaturalHeight(node);
44
+ const key = imageAssetKey(src, htmlDir, width, height);
45
+ let assetIdx = keyToAssetIndex.get(key);
46
+ if (assetIdx === undefined) {
47
+ const assetId = uniqueAssetId(node.id, assets.length, usedIds);
48
+ const asset = readRgb565Image(src, htmlDir, assetId, width, height);
49
+ if (asset) {
50
+ assetIdx = assets.length;
51
+ assets.push(asset);
52
+ keyToAssetIndex.set(key, assetIdx);
53
+ }
54
+ }
55
+ if (assetIdx !== undefined) {
56
+ nodeIdToAssetIndex.set(node.id, assetIdx);
57
+ }
58
+ }
59
+ node.children.forEach(walk);
60
+ };
61
+ for (const root of Array.isArray(roots) ? roots : [roots]) {
62
+ walk(root);
63
+ }
64
+ return { assets, nodeIdToAssetIndex };
65
+ }
66
+ function rgb565ToRgb888(v) {
67
+ const r5 = (v >> 11) & 0x1f;
68
+ const g6 = (v >> 5) & 0x3f;
69
+ const b5 = v & 0x1f;
70
+ const r = (r5 << 3) | (r5 >> 2);
71
+ const g = (g6 << 2) | (g6 >> 4);
72
+ const b = (b5 << 3) | (b5 >> 2);
73
+ return (r << 16) | (g << 8) | b;
74
+ }
75
+ function emitPixelValue(v, colorFormat) {
76
+ if (colorFormat === "rgb666" || colorFormat === "rgb888") {
77
+ return "0x" + rgb565ToRgb888(v).toString(16).padStart(6, "0");
78
+ }
79
+ return "0x" + (v & 0xffff).toString(16).padStart(4, "0");
80
+ }
81
+ export function emitImageTables(assets, colorFormat = "rgb565") {
82
+ if (assets.length === 0) {
83
+ return "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
84
+ }
85
+ const lines = [];
86
+ for (const asset of assets) {
87
+ lines.push(`static const UI_COLOR_T __ui_img_${asset.id}_data[] = {`);
88
+ for (let i = 0; i < asset.data.length; i += 16) {
89
+ const chunk = asset.data
90
+ .slice(i, i + 16)
91
+ .map((v) => emitPixelValue(v, colorFormat));
92
+ lines.push(" " + chunk.join(", ") + ",");
93
+ }
94
+ lines.push("};");
95
+ }
96
+ lines.push("const UIImage __ui_images[] = {");
97
+ for (const asset of assets) {
98
+ lines.push(` { ${asset.width}, ${asset.height}, __ui_img_${asset.id}_data },`);
99
+ }
100
+ lines.push("};");
101
+ lines.push(`const uint16_t __ui_image_count = ${assets.length};`);
102
+ return lines.join("\n");
103
+ }
104
+ /** Read a raw RGB565 binary file and return a padded UIImageAsset. */
105
+ export function readRgb565Image(srcPath, htmlDir, id, width, height) {
106
+ const abs = path.isAbsolute(srcPath)
107
+ ? srcPath
108
+ : path.resolve(htmlDir, srcPath);
109
+ if (!fs.existsSync(abs)) {
110
+ console.warn(`[img] Image file not found: ${abs}`);
111
+ return null;
112
+ }
113
+ const buf = fs.readFileSync(abs);
114
+ const expectedPixels = width * height;
115
+ const expectedBytes = expectedPixels * 2;
116
+ if (buf.length < expectedBytes) {
117
+ console.warn(`[img] Image ${srcPath} is ${buf.length} bytes, expected ${expectedBytes} (${width}x${height}x2). Padding missing pixels with 0.`);
118
+ }
119
+ const availablePixels = Math.min(expectedPixels, Math.floor(buf.length / 2));
120
+ const data = new Array(expectedPixels).fill(0);
121
+ for (let i = 0; i < availablePixels; i++) {
122
+ data[i] = buf.readUInt16LE(i * 2);
123
+ }
124
+ return { id, width, height, data };
125
+ }
@@ -0,0 +1,30 @@
1
+ import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
2
+ /** Tags whose content participates in inline flow inside a text container.
3
+ * Bare text nodes between them flow too. Recognized as inline children of a
4
+ * text-effective node; absorbed into the parent's run list by the resolver. */
5
+ export declare const INLINE_TAGS: Set<string>;
6
+ export type InlineItem = {
7
+ kind: "text";
8
+ text: string;
9
+ } | {
10
+ kind: "element";
11
+ tag: string;
12
+ origTag?: string;
13
+ classes: string[];
14
+ inlineStyle?: string;
15
+ href?: string;
16
+ inline?: InlineItem[];
17
+ } | {
18
+ kind: "break";
19
+ };
20
+ /** Walk childNodes in document order, building an inline sequence.
21
+ *
22
+ * Returns `undefined` when the element is NOT inline-bearing:
23
+ * - has any block-level child (a non-inline, non-br element) → block breaks
24
+ * the flow, so this element is treated as a container, not a rich-text node;
25
+ * - has no recognized inline element child at all → plain text, handled by the
26
+ * existing single-string text path.
27
+ *
28
+ * Otherwise returns the ordered sequence of text fragments, inline elements
29
+ * (recursively), and hard-break markers (`<br>`). */
30
+ export declare function collectInlineSequence(el: Element, diagnostics: Diagnostic[]): InlineItem[] | undefined;
@@ -0,0 +1,66 @@
1
+ /** Tags whose content participates in inline flow inside a text container.
2
+ * Bare text nodes between them flow too. Recognized as inline children of a
3
+ * text-effective node; absorbed into the parent's run list by the resolver. */
4
+ export const INLINE_TAGS = new Set(["span", "a", "b", "strong", "i", "em", "u"]);
5
+ /** Walk childNodes in document order, building an inline sequence.
6
+ *
7
+ * Returns `undefined` when the element is NOT inline-bearing:
8
+ * - has any block-level child (a non-inline, non-br element) → block breaks
9
+ * the flow, so this element is treated as a container, not a rich-text node;
10
+ * - has no recognized inline element child at all → plain text, handled by the
11
+ * existing single-string text path.
12
+ *
13
+ * Otherwise returns the ordered sequence of text fragments, inline elements
14
+ * (recursively), and hard-break markers (`<br>`). */
15
+ export function collectInlineSequence(el, diagnostics) {
16
+ const childElements = Array.from(el.children);
17
+ const hasBlockChild = childElements.some((c) => !INLINE_TAGS.has(c.tagName.toLowerCase()) && c.tagName.toLowerCase() !== "br");
18
+ if (hasBlockChild)
19
+ return undefined;
20
+ // Only build a sequence when there's at least one recognized inline element
21
+ // child OR a <br> among mixed text (a <br> mid-paragraph is inline content
22
+ // that needs the rich-text path to render as a hard line break). A node with
23
+ // only bare text nodes (no elements, no <br>) is plain text — existing path.
24
+ const hasInlineChild = childElements.some((c) => INLINE_TAGS.has(c.tagName.toLowerCase()) || c.tagName.toLowerCase() === "br");
25
+ if (!hasInlineChild)
26
+ return undefined;
27
+ const seq = [];
28
+ for (const child of Array.from(el.childNodes)) {
29
+ const nodeType = child.nodeType;
30
+ if (nodeType === 3) { // text node
31
+ const text = child.textContent ?? "";
32
+ if (text)
33
+ seq.push({ kind: "text", text });
34
+ continue;
35
+ }
36
+ const childTag = child.tagName?.toLowerCase();
37
+ if (childTag === "br") {
38
+ seq.push({ kind: "break" });
39
+ continue;
40
+ }
41
+ if (childTag && INLINE_TAGS.has(childTag)) {
42
+ const cel = child;
43
+ const classes = (cel.getAttribute("class") || "").split(/\s+/).filter(Boolean);
44
+ let subInline = collectInlineSequence(cel, diagnostics);
45
+ // A leaf inline element (only text, no nested inline children) collects
46
+ // no sub-sequence — synthesize one text item from its text content so the
47
+ // resolver emits a run for it. Without this, <b>world</b> would carry no
48
+ // text and produce no run.
49
+ if (subInline === undefined) {
50
+ const leafText = cel.textContent ?? "";
51
+ subInline = leafText ? [{ kind: "text", text: leafText }] : [];
52
+ }
53
+ seq.push({
54
+ kind: "element",
55
+ tag: "text",
56
+ origTag: childTag,
57
+ classes,
58
+ inlineStyle: cel.getAttribute("style") || undefined,
59
+ href: childTag === "a" ? (cel.getAttribute("href") || undefined) : undefined,
60
+ inline: subInline,
61
+ });
62
+ }
63
+ // Other node types (comments, unknown elements already filtered) are ignored.
64
+ }
65
+ return seq;
66
+ }
@@ -0,0 +1,3 @@
1
+ import type { KeyframeSet } from "./css-parser.js";
2
+ import { type KeyframeSetModel } from "./model.js";
3
+ export declare function buildKeyframeSets(rawKeyframes: KeyframeSet[], colorFormat: "rgb565" | "rgb666" | "rgb888" | "mono"): KeyframeSetModel[];