embedded-react 0.3.0 → 0.4.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.
@@ -25,24 +25,29 @@
25
25
  // Prereq: the linux-aot CMake build must be configurable (SDL2 found). It reuses examples/linux-aot/build;
26
26
  // if that isn't configured yet, set CMAKE_TOOLCHAIN_FILE (e.g. a vcpkg toolchain) and it will configure it.
27
27
  // Needs a display (SDL video); on a headless box run under a virtual framebuffer.
28
- import { execFileSync } from 'node:child_process';
29
- import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
30
- import { resolve, dirname } from 'node:path';
31
- import { fileURLToPath } from 'node:url';
28
+ import {execFileSync} from 'node:child_process';
29
+ import {existsSync, mkdirSync, readFileSync, rmSync} from 'node:fs';
30
+ import {resolve, dirname} from 'node:path';
31
+ import {fileURLToPath} from 'node:url';
32
32
 
33
33
  const here = dirname(fileURLToPath(import.meta.url)); // bridges/quickjs/js/aot
34
34
  const jsDir = resolve(here, '..'); // bridges/quickjs/js
35
35
  const repoRoot = resolve(here, '../../../..');
36
36
  const exampleDir = resolve(repoRoot, 'examples/linux-aot');
37
37
  const buildDir = resolve(exampleDir, 'build');
38
- const exe = resolve(buildDir, process.platform === 'win32' ? 'embedded-react-desktop-aot.exe' : 'embedded-react-desktop-aot');
38
+ const exe = resolve(
39
+ buildDir,
40
+ process.platform === 'win32'
41
+ ? 'embedded-react-desktop-aot.exe'
42
+ : 'embedded-react-desktop-aot',
43
+ );
39
44
  const tmpDir = resolve(jsDir, 'dist', '.smoke');
40
45
 
41
46
  // Demos to smoke-test. `screen` (optional) sets ER_AOT_SCREEN_W/H so the responsive thermostat folds to its
42
47
  // compact (AOT-compilable) branch. `minColors` is the floor of distinct sampled colours for "rendered".
43
48
  const DEMOS = [
44
- { name: 'music-player', minColors: 40 },
45
- { name: 'thermostat', screen: { w: 240, h: 320 }, minColors: 40 },
49
+ {name: 'music-player', minColors: 40},
50
+ {name: 'thermostat', screen: {w: 240, h: 320}, minColors: 40},
46
51
  ];
47
52
 
48
53
  /** Counts distinct colours in an uncompressed 24/32-bpp BMP (sampled) — a quick "is anything drawn?" signal. */
@@ -67,38 +72,46 @@ function bmpDistinctColors(path, step = 4) {
67
72
  }
68
73
 
69
74
  function run(cmd, args, opts = {}) {
70
- execFileSync(cmd, args, { stdio: 'pipe', ...opts });
75
+ execFileSync(cmd, args, {stdio: 'pipe', ...opts});
71
76
  }
72
77
 
73
78
  function ensureConfigured() {
74
79
  if (existsSync(resolve(buildDir, 'CMakeCache.txt'))) return;
75
80
  console.log('• configuring linux-aot build (first run)…');
76
81
  const args = ['-S', exampleDir, '-B', buildDir];
77
- if (process.env.CMAKE_TOOLCHAIN_FILE) args.push(`-DCMAKE_TOOLCHAIN_FILE=${process.env.CMAKE_TOOLCHAIN_FILE}`);
82
+ if (process.env.CMAKE_TOOLCHAIN_FILE)
83
+ args.push(`-DCMAKE_TOOLCHAIN_FILE=${process.env.CMAKE_TOOLCHAIN_FILE}`);
78
84
  if (process.platform === 'win32') args.push('-G', 'MinGW Makefiles');
79
85
  run('cmake', args);
80
86
  }
81
87
 
82
88
  let failures = 0;
83
- mkdirSync(tmpDir, { recursive: true });
89
+ mkdirSync(tmpDir, {recursive: true});
84
90
  ensureConfigured();
85
91
 
86
92
  for (const demo of DEMOS) {
87
93
  const shot = resolve(tmpDir, `${demo.name}.bmp`);
88
94
  try {
89
- rmSync(shot, { force: true });
90
- const genEnv = { ...process.env };
95
+ rmSync(shot, {force: true});
96
+ const genEnv = {...process.env};
91
97
  if (demo.screen) {
92
98
  genEnv.ER_AOT_SCREEN_W = String(demo.screen.w);
93
99
  genEnv.ER_AOT_SCREEN_H = String(demo.screen.h);
94
100
  }
95
- run('node', [resolve(here, 'compile.mjs'), demo.name], { cwd: jsDir, env: genEnv }); // → dist/app.gen.{c,h}
101
+ run('node', [resolve(here, 'compile.mjs'), demo.name], {
102
+ cwd: jsDir,
103
+ env: genEnv,
104
+ }); // → dist/app.gen.{c,h}
96
105
  run('cmake', ['--build', buildDir]); // relink the generated C
97
- run(exe, [], { cwd: buildDir, env: { ...process.env, ER_AOT_SHOT: shot } }); // render one frame → BMP
106
+ run(exe, [], {cwd: buildDir, env: {...process.env, ER_AOT_SHOT: shot}}); // render one frame → BMP
98
107
 
99
- if (!existsSync(shot)) throw new Error('no screenshot written (host crashed before present?)');
108
+ if (!existsSync(shot))
109
+ throw new Error('no screenshot written (host crashed before present?)');
100
110
  const colors = bmpDistinctColors(shot);
101
- if (colors < demo.minColors) throw new Error(`screenshot looks blank (${colors} distinct colours < ${demo.minColors})`);
111
+ if (colors < demo.minColors)
112
+ throw new Error(
113
+ `screenshot looks blank (${colors} distinct colours < ${demo.minColors})`,
114
+ );
102
115
  console.log(`✓ ${demo.name}: rendered (${colors} distinct colours)`);
103
116
  } catch (e) {
104
117
  failures++;
@@ -106,5 +119,9 @@ for (const demo of DEMOS) {
106
119
  }
107
120
  }
108
121
 
109
- console.log(failures ? `\n${failures} demo(s) failed the smoke test.` : `\nAll ${DEMOS.length} demos compiled, built, and rendered.`);
122
+ console.log(
123
+ failures
124
+ ? `\n${failures} demo(s) failed the smoke test.`
125
+ : `\nAll ${DEMOS.length} demos compiled, built, and rendered.`,
126
+ );
110
127
  process.exit(failures ? 1 : 0);
package/aot/style-map.mjs CHANGED
@@ -42,13 +42,21 @@ const NAMED_COLORS = {
42
42
  * @returns {number} ARGB8888 as an unsigned 32-bit integer.
43
43
  */
44
44
  export function parseColorValue(s) {
45
- if (typeof s !== 'string') throw new Error(`color must be a string, got ${typeof s}`);
45
+ if (typeof s !== 'string')
46
+ throw new Error(`color must be a string, got ${typeof s}`);
46
47
  const key = s.trim().toLowerCase();
47
48
  if (key in NAMED_COLORS) return NAMED_COLORS[key] >>> 0;
48
49
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(key);
49
- if (!m) throw new Error(`unsupported color "${s}" (use #rgb / #rrggbb / #rrggbbaa or a named color)`);
50
+ if (!m)
51
+ throw new Error(
52
+ `unsupported color "${s}" (use #rgb / #rrggbb / #rrggbbaa or a named color)`,
53
+ );
50
54
  let hex = m[1];
51
- if (hex.length === 3) hex = hex.split('').map((c) => c + c).join(''); // #rgb → #rrggbb
55
+ if (hex.length === 3)
56
+ hex = hex
57
+ .split('')
58
+ .map(c => c + c)
59
+ .join(''); // #rgb → #rrggbb
52
60
  let r, g, b, a;
53
61
  if (hex.length === 6) {
54
62
  a = 0xff;
@@ -62,7 +70,7 @@ export function parseColorValue(s) {
62
70
  b = parseInt(hex.slice(4, 6), 16);
63
71
  a = parseInt(hex.slice(6, 8), 16);
64
72
  }
65
- return (((a << 24) | (r << 16) | (g << 8) | b) >>> 0);
73
+ return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
66
74
  }
67
75
 
68
76
  /** Formats an ARGB int as a C unsigned hex literal, e.g. 0xFF0F172Au. */
@@ -91,15 +99,19 @@ const FLEX_DIRECTION = {
91
99
  'row-reverse': 'ER_FLEX_ROW_REVERSE',
92
100
  'column-reverse': 'ER_FLEX_COL_REVERSE',
93
101
  };
94
- const POSITION = { relative: 'ER_POS_RELATIVE', absolute: 'ER_POS_ABSOLUTE' };
102
+ const POSITION = {relative: 'ER_POS_RELATIVE', absolute: 'ER_POS_ABSOLUTE'};
95
103
 
96
- const enumKey = (table, name) => (v) => {
104
+ const enumKey = (table, name) => v => {
97
105
  const c = table[v];
98
- if (!c) throw new Error(`${name}: unsupported value "${v}" (one of ${Object.keys(table).join(', ')})`);
106
+ if (!c)
107
+ throw new Error(
108
+ `${name}: unsupported value "${v}" (one of ${Object.keys(table).join(', ')})`,
109
+ );
99
110
  return c;
100
111
  };
101
- const dim = (v) => {
102
- if (typeof v !== 'number' || !Number.isFinite(v)) throw new Error(`expected a numeric dimension, got ${JSON.stringify(v)}`);
112
+ const dim = v => {
113
+ if (typeof v !== 'number' || !Number.isFinite(v))
114
+ throw new Error(`expected a numeric dimension, got ${JSON.stringify(v)}`);
103
115
  return String(Math.round(v));
104
116
  };
105
117
 
@@ -110,15 +122,18 @@ const dim = (v) => {
110
122
  * @param {string} pxField ERProps pixel field (e.g. 'width').
111
123
  * @param {string} pctField ERProps percentage field (e.g. 'width_pct').
112
124
  */
113
- const pctOrPx = (pxField, pctField) => (v) => {
125
+ const pctOrPx = (pxField, pctField) => v => {
114
126
  if (typeof v === 'string' && v.trim().endsWith('%')) {
115
127
  const n = parseFloat(v);
116
- if (!Number.isFinite(n)) throw new Error(`expected a percentage like '50%', got ${JSON.stringify(v)}`);
128
+ if (!Number.isFinite(n))
129
+ throw new Error(
130
+ `expected a percentage like '50%', got ${JSON.stringify(v)}`,
131
+ );
117
132
  // A valid C float literal needs a decimal point — `50f` is a syntax error, `50.0f` is not.
118
133
  const lit = Number.isInteger(n) ? `${n}.0f` : `${n}f`;
119
- return [{ field: pctField, expr: lit }];
134
+ return [{field: pctField, expr: lit}];
120
135
  }
121
- return [{ field: pxField, expr: dim(v) }];
136
+ return [{field: pxField, expr: dim(v)}];
122
137
  };
123
138
 
124
139
  /**
@@ -130,61 +145,89 @@ const KEYS = {
130
145
  width: pctOrPx('width', 'width_pct'),
131
146
  height: pctOrPx('height', 'height_pct'),
132
147
  flexBasis: pctOrPx('flex_basis', 'flex_basis_pct'),
133
- minWidth: (v) => [{ field: 'min_width', expr: dim(v) }],
134
- maxWidth: (v) => [{ field: 'max_width', expr: dim(v) }],
135
- minHeight: (v) => [{ field: 'min_height', expr: dim(v) }],
136
- maxHeight: (v) => [{ field: 'max_height', expr: dim(v) }],
137
- padding: (v) => [{ field: 'padding', expr: dim(v) }],
138
- paddingHorizontal: (v) => [{ field: 'padding_horizontal', expr: dim(v) }],
139
- paddingVertical: (v) => [{ field: 'padding_vertical', expr: dim(v) }],
140
- paddingLeft: (v) => [{ field: 'padding_left', expr: dim(v) }],
141
- paddingTop: (v) => [{ field: 'padding_top', expr: dim(v) }],
142
- paddingRight: (v) => [{ field: 'padding_right', expr: dim(v) }],
143
- paddingBottom: (v) => [{ field: 'padding_bottom', expr: dim(v) }],
144
- margin: (v) => [{ field: 'margin', expr: dim(v) }],
145
- marginHorizontal: (v) => [{ field: 'margin_horizontal', expr: dim(v) }],
146
- marginVertical: (v) => [{ field: 'margin_vertical', expr: dim(v) }],
147
- marginLeft: (v) => [{ field: 'margin_left', expr: dim(v) }],
148
- marginTop: (v) => [{ field: 'margin_top', expr: dim(v) }],
149
- marginRight: (v) => [{ field: 'margin_right', expr: dim(v) }],
150
- marginBottom: (v) => [{ field: 'margin_bottom', expr: dim(v) }],
151
- gap: (v) => [{ field: 'gap', expr: dim(v) }],
152
- rowGap: (v) => [{ field: 'row_gap', expr: dim(v) }],
153
- columnGap: (v) => [{ field: 'column_gap', expr: dim(v) }],
154
- flexGrow: (v) => [{ field: 'flex_grow', expr: dim(v) }],
155
- flexShrink: (v) => [{ field: 'flex_shrink', expr: dim(v) }],
156
- flexDirection: (v) => [{ field: 'flex_direction', expr: enumKey(FLEX_DIRECTION, 'flexDirection')(v) }],
157
- alignItems: (v) => [{ field: 'align_items', expr: enumKey(ALIGN, 'alignItems')(v) }],
158
- alignSelf: (v) => [{ field: 'align_self', expr: enumKey(ALIGN, 'alignSelf')(v) }],
159
- justifyContent: (v) => [{ field: 'justify_content', expr: enumKey(JUSTIFY, 'justifyContent')(v) }],
148
+ minWidth: v => [{field: 'min_width', expr: dim(v)}],
149
+ maxWidth: v => [{field: 'max_width', expr: dim(v)}],
150
+ minHeight: v => [{field: 'min_height', expr: dim(v)}],
151
+ maxHeight: v => [{field: 'max_height', expr: dim(v)}],
152
+ padding: v => [{field: 'padding', expr: dim(v)}],
153
+ paddingHorizontal: v => [{field: 'padding_horizontal', expr: dim(v)}],
154
+ paddingVertical: v => [{field: 'padding_vertical', expr: dim(v)}],
155
+ paddingLeft: v => [{field: 'padding_left', expr: dim(v)}],
156
+ paddingTop: v => [{field: 'padding_top', expr: dim(v)}],
157
+ paddingRight: v => [{field: 'padding_right', expr: dim(v)}],
158
+ paddingBottom: v => [{field: 'padding_bottom', expr: dim(v)}],
159
+ margin: v => [{field: 'margin', expr: dim(v)}],
160
+ marginHorizontal: v => [{field: 'margin_horizontal', expr: dim(v)}],
161
+ marginVertical: v => [{field: 'margin_vertical', expr: dim(v)}],
162
+ marginLeft: v => [{field: 'margin_left', expr: dim(v)}],
163
+ marginTop: v => [{field: 'margin_top', expr: dim(v)}],
164
+ marginRight: v => [{field: 'margin_right', expr: dim(v)}],
165
+ marginBottom: v => [{field: 'margin_bottom', expr: dim(v)}],
166
+ gap: v => [{field: 'gap', expr: dim(v)}],
167
+ rowGap: v => [{field: 'row_gap', expr: dim(v)}],
168
+ columnGap: v => [{field: 'column_gap', expr: dim(v)}],
169
+ flexGrow: v => [{field: 'flex_grow', expr: dim(v)}],
170
+ flexShrink: v => [{field: 'flex_shrink', expr: dim(v)}],
171
+ flexDirection: v => [
172
+ {
173
+ field: 'flex_direction',
174
+ expr: enumKey(FLEX_DIRECTION, 'flexDirection')(v),
175
+ },
176
+ ],
177
+ alignItems: v => [
178
+ {field: 'align_items', expr: enumKey(ALIGN, 'alignItems')(v)},
179
+ ],
180
+ alignSelf: v => [{field: 'align_self', expr: enumKey(ALIGN, 'alignSelf')(v)}],
181
+ justifyContent: v => [
182
+ {field: 'justify_content', expr: enumKey(JUSTIFY, 'justifyContent')(v)},
183
+ ],
160
184
  // Positioning: `position: 'absolute'` takes a node out of flow; left/top/right/bottom are its anchors.
161
- position: (v) => [{ field: 'position', expr: enumKey(POSITION, 'position')(v) }],
162
- left: (v) => [{ field: 'left', expr: dim(v) }],
163
- top: (v) => [{ field: 'top', expr: dim(v) }],
164
- right: (v) => [{ field: 'right', expr: dim(v) }],
165
- bottom: (v) => [{ field: 'bottom', expr: dim(v) }],
185
+ position: v => [{field: 'position', expr: enumKey(POSITION, 'position')(v)}],
186
+ left: v => [{field: 'left', expr: dim(v)}],
187
+ top: v => [{field: 'top', expr: dim(v)}],
188
+ right: v => [{field: 'right', expr: dim(v)}],
189
+ bottom: v => [{field: 'bottom', expr: dim(v)}],
166
190
  // `flex: n` → grow=n, shrink=1, basis=0 (RN semantics; matches native_ui_bridge apply_flex).
167
- flex: (v) => {
191
+ flex: v => {
168
192
  const n = Number(v);
169
- if (n > 0) return [{ field: 'flex_grow', expr: dim(n) }, { field: 'flex_shrink', expr: '1' }, { field: 'flex_basis', expr: '0' }];
170
- if (n === 0) return [{ field: 'flex_grow', expr: '0' }, { field: 'flex_shrink', expr: '0' }];
171
- return [{ field: 'flex_grow', expr: '0' }, { field: 'flex_shrink', expr: '1' }];
193
+ if (n > 0)
194
+ return [
195
+ {field: 'flex_grow', expr: dim(n)},
196
+ {field: 'flex_shrink', expr: '1'},
197
+ {field: 'flex_basis', expr: '0'},
198
+ ];
199
+ if (n === 0)
200
+ return [
201
+ {field: 'flex_grow', expr: '0'},
202
+ {field: 'flex_shrink', expr: '0'},
203
+ ];
204
+ return [
205
+ {field: 'flex_grow', expr: '0'},
206
+ {field: 'flex_shrink', expr: '1'},
207
+ ];
172
208
  },
173
209
 
174
210
  // View visual
175
- backgroundColor: (v) => [{ field: 'background_color', expr: colorLiteral(v) }],
176
- borderRadius: (v) => [{ field: 'border_radius', expr: dim(v) }],
177
- borderWidth: (v) => [{ field: 'border_width', expr: dim(v) }],
178
- borderColor: (v) => [{ field: 'border_color', expr: colorLiteral(v) }],
179
- opacity: (v) => [{ field: 'opacity', expr: String(Math.round(Math.max(0, Math.min(1, Number(v))) * 255)) }],
180
- zIndex: (v) => [{ field: 'z_index', expr: dim(v) }],
211
+ backgroundColor: v => [{field: 'background_color', expr: colorLiteral(v)}],
212
+ borderRadius: v => [{field: 'border_radius', expr: dim(v)}],
213
+ borderWidth: v => [{field: 'border_width', expr: dim(v)}],
214
+ borderColor: v => [{field: 'border_color', expr: colorLiteral(v)}],
215
+ opacity: v => [
216
+ {
217
+ field: 'opacity',
218
+ expr: String(Math.round(Math.max(0, Math.min(1, Number(v))) * 255)),
219
+ },
220
+ ],
221
+ zIndex: v => [{field: 'z_index', expr: dim(v)}],
181
222
 
182
223
  // Text
183
- color: (v) => [{ field: 'color', expr: colorLiteral(v) }],
184
- fontSize: (v) => [{ field: 'font_size', expr: dim(v) }],
185
- fontWeight: (v) => [{ field: 'font_weight', expr: v === 'bold' || Number(v) >= 600 ? '1' : '0' }],
186
- lineHeight: (v) => [{ field: 'line_height', expr: dim(v) }],
187
- letterSpacing: (v) => [{ field: 'letter_spacing', expr: dim(v) }],
224
+ color: v => [{field: 'color', expr: colorLiteral(v)}],
225
+ fontSize: v => [{field: 'font_size', expr: dim(v)}],
226
+ fontWeight: v => [
227
+ {field: 'font_weight', expr: v === 'bold' || Number(v) >= 600 ? '1' : '0'},
228
+ ],
229
+ lineHeight: v => [{field: 'line_height', expr: dim(v)}],
230
+ letterSpacing: v => [{field: 'letter_spacing', expr: dim(v)}],
188
231
  };
189
232
 
190
233
  /**
@@ -198,7 +241,10 @@ export function lowerStyle(style) {
198
241
  for (const [key, value] of Object.entries(style)) {
199
242
  if (value === undefined || value === null) continue;
200
243
  const fn = KEYS[key];
201
- if (!fn) throw new Error(`AOT: unsupported style key "${key}" (not yet lowered to ERProps)`);
244
+ if (!fn)
245
+ throw new Error(
246
+ `AOT: unsupported style key "${key}" (not yet lowered to ERProps)`,
247
+ );
202
248
  out.push(...fn(value));
203
249
  }
204
250
  return out;
@@ -218,31 +264,61 @@ export const NODE_TYPES = {
218
264
  // 'opacity' → scale a 0–1 expression to 0–255; 'color' → a (ternary of) color literal(s). Keys absent
219
265
  // here (enums like flexDirection, the `flex` shorthand) can only be static for now.
220
266
  const NUM_FIELDS = {
221
- width: 'width', height: 'height', minWidth: 'min_width', maxWidth: 'max_width', minHeight: 'min_height', maxHeight: 'max_height',
222
- padding: 'padding', paddingHorizontal: 'padding_horizontal', paddingVertical: 'padding_vertical',
223
- paddingLeft: 'padding_left', paddingTop: 'padding_top', paddingRight: 'padding_right', paddingBottom: 'padding_bottom',
224
- margin: 'margin', marginHorizontal: 'margin_horizontal', marginVertical: 'margin_vertical',
225
- marginLeft: 'margin_left', marginTop: 'margin_top', marginRight: 'margin_right', marginBottom: 'margin_bottom',
226
- gap: 'gap', rowGap: 'row_gap', columnGap: 'column_gap', flexGrow: 'flex_grow', flexShrink: 'flex_shrink',
227
- borderRadius: 'border_radius', borderWidth: 'border_width', zIndex: 'z_index',
228
- fontSize: 'font_size', lineHeight: 'line_height', letterSpacing: 'letter_spacing',
267
+ width: 'width',
268
+ height: 'height',
269
+ minWidth: 'min_width',
270
+ maxWidth: 'max_width',
271
+ minHeight: 'min_height',
272
+ maxHeight: 'max_height',
273
+ padding: 'padding',
274
+ paddingHorizontal: 'padding_horizontal',
275
+ paddingVertical: 'padding_vertical',
276
+ paddingLeft: 'padding_left',
277
+ paddingTop: 'padding_top',
278
+ paddingRight: 'padding_right',
279
+ paddingBottom: 'padding_bottom',
280
+ margin: 'margin',
281
+ marginHorizontal: 'margin_horizontal',
282
+ marginVertical: 'margin_vertical',
283
+ marginLeft: 'margin_left',
284
+ marginTop: 'margin_top',
285
+ marginRight: 'margin_right',
286
+ marginBottom: 'margin_bottom',
287
+ gap: 'gap',
288
+ rowGap: 'row_gap',
289
+ columnGap: 'column_gap',
290
+ flexGrow: 'flex_grow',
291
+ flexShrink: 'flex_shrink',
292
+ borderRadius: 'border_radius',
293
+ borderWidth: 'border_width',
294
+ zIndex: 'z_index',
295
+ fontSize: 'font_size',
296
+ lineHeight: 'line_height',
297
+ letterSpacing: 'letter_spacing',
229
298
  };
230
299
 
231
300
  // Enum style keys that can be state-driven: the value (a string literal or a ternary of them) lowers to the
232
301
  // matching ER_* enum constant via its table. Changing one in app_update re-runs layout (props_hash covers it).
233
302
  const ENUM_FIELDS = {
234
- flexDirection: { field: 'flex_direction', table: FLEX_DIRECTION },
235
- alignItems: { field: 'align_items', table: ALIGN },
236
- alignSelf: { field: 'align_self', table: ALIGN },
237
- justifyContent: { field: 'justify_content', table: JUSTIFY },
238
- position: { field: 'position', table: POSITION },
303
+ flexDirection: {field: 'flex_direction', table: FLEX_DIRECTION},
304
+ alignItems: {field: 'align_items', table: ALIGN},
305
+ alignSelf: {field: 'align_self', table: ALIGN},
306
+ justifyContent: {field: 'justify_content', table: JUSTIFY},
307
+ position: {field: 'position', table: POSITION},
239
308
  };
240
309
 
241
310
  export const DYN_FIELDS = {
242
- ...Object.fromEntries(Object.entries(NUM_FIELDS).map(([k, f]) => [k, { field: f, kind: 'num' }])),
243
- ...Object.fromEntries(Object.entries(ENUM_FIELDS).map(([k, m]) => [k, { field: m.field, kind: 'enum', table: m.table }])),
244
- backgroundColor: { field: 'background_color', kind: 'color' },
245
- color: { field: 'color', kind: 'color' },
246
- borderColor: { field: 'border_color', kind: 'color' },
247
- opacity: { field: 'opacity', kind: 'opacity' },
311
+ ...Object.fromEntries(
312
+ Object.entries(NUM_FIELDS).map(([k, f]) => [k, {field: f, kind: 'num'}]),
313
+ ),
314
+ ...Object.fromEntries(
315
+ Object.entries(ENUM_FIELDS).map(([k, m]) => [
316
+ k,
317
+ {field: m.field, kind: 'enum', table: m.table},
318
+ ]),
319
+ ),
320
+ backgroundColor: {field: 'background_color', kind: 'color'},
321
+ color: {field: 'color', kind: 'color'},
322
+ borderColor: {field: 'border_color', kind: 'color'},
323
+ opacity: {field: 'opacity', kind: 'opacity'},
248
324
  };
@@ -26,7 +26,7 @@
26
26
  // bpp 8 → 1 byte/px, raw coverage
27
27
  import fs from 'node:fs';
28
28
  import opentype from 'opentype.js';
29
- import { rasterize } from './rasterize.mjs';
29
+ import {rasterize} from './rasterize.mjs';
30
30
 
31
31
  /** Default dense glyph range: printable ASCII (0x20..0x7E). */
32
32
  export const ASCII_FIRST = 0x20;
@@ -34,14 +34,16 @@ export const ASCII_LAST = 0x7e;
34
34
 
35
35
  // Named extra-symbol sets selectable from assets.config.js (glyphs: 'common' | 'minimal' | ...).
36
36
  const SYMBOLS_MINIMAL = [
37
- 0x00b0, 0x00b1, 0x00b5, 0x00d7, 0x00f7, 0x2013, 0x2014, 0x2022, 0x2026, 0x2190, 0x2191, 0x2192,
38
- 0x2193, 0x2713, 0x2717,
37
+ 0x00b0, 0x00b1, 0x00b5, 0x00d7, 0x00f7, 0x2013, 0x2014, 0x2022, 0x2026,
38
+ 0x2190, 0x2191, 0x2192, 0x2193, 0x2713, 0x2717,
39
39
  ];
40
40
  const SYMBOLS_COMMON = [
41
- 0x00a2, 0x00a3, 0x00a5, 0x00a7, 0x00a9, 0x00ae, 0x00b0, 0x00b1, 0x00b5, 0x00d7, 0x00f7, 0x2013,
42
- 0x2014, 0x2018, 0x2019, 0x201c, 0x201d, 0x2020, 0x2021, 0x2022, 0x2026, 0x2030, 0x20ac, 0x2122,
43
- 0x2190, 0x2191, 0x2192, 0x2193, 0x2194, 0x21b5, 0x2202, 0x2206, 0x221a, 0x221e, 0x2211, 0x2212,
44
- 0x2248, 0x2260, 0x2264, 0x2265, 0x25a0, 0x25cf, 0x25c6, 0x2605, 0x2606, 0x2713, 0x2717,
41
+ 0x00a2, 0x00a3, 0x00a5, 0x00a7, 0x00a9, 0x00ae, 0x00b0, 0x00b1, 0x00b5,
42
+ 0x00d7, 0x00f7, 0x2013, 0x2014, 0x2018, 0x2019, 0x201c, 0x201d, 0x2020,
43
+ 0x2021, 0x2022, 0x2026, 0x2030, 0x20ac, 0x2122, 0x2190, 0x2191, 0x2192,
44
+ 0x2193, 0x2194, 0x21b5, 0x2202, 0x2206, 0x221a, 0x221e, 0x2211, 0x2212,
45
+ 0x2248, 0x2260, 0x2264, 0x2265, 0x25a0, 0x25cf, 0x25c6, 0x2605, 0x2606,
46
+ 0x2713, 0x2717,
45
47
  ];
46
48
  const SYMBOLS_GREEK = [];
47
49
  for (let c = 0x0391; c <= 0x03a9; c++) if (c !== 0x03a2) SYMBOLS_GREEK.push(c); // uppercase (skip gap)
@@ -51,7 +53,9 @@ const SYMBOL_SETS = {
51
53
  minimal: SYMBOLS_MINIMAL,
52
54
  common: SYMBOLS_COMMON,
53
55
  greek: SYMBOLS_GREEK,
54
- 'common-greek': [...new Set([...SYMBOLS_COMMON, ...SYMBOLS_GREEK])].sort((a, b) => a - b),
56
+ 'common-greek': [...new Set([...SYMBOLS_COMMON, ...SYMBOLS_GREEK])].sort(
57
+ (a, b) => a - b,
58
+ ),
55
59
  };
56
60
 
57
61
  /**
@@ -65,8 +69,13 @@ export function resolveExtras(glyphs) {
65
69
  let cps;
66
70
  if (Array.isArray(glyphs)) cps = glyphs.slice();
67
71
  else if (SYMBOL_SETS[glyphs]) cps = SYMBOL_SETS[glyphs].slice();
68
- else throw new Error(`unknown glyph set "${glyphs}" (use 'ascii', a named set, or a codepoint array)`);
69
- return [...new Set(cps)].filter((c) => c < ASCII_FIRST || c > ASCII_LAST).sort((a, b) => a - b);
72
+ else
73
+ throw new Error(
74
+ `unknown glyph set "${glyphs}" (use 'ascii', a named set, or a codepoint array)`,
75
+ );
76
+ return [...new Set(cps)]
77
+ .filter(c => c < ASCII_FIRST || c > ASCII_LAST)
78
+ .sort((a, b) => a - b);
70
79
  }
71
80
 
72
81
  /**
@@ -94,7 +103,8 @@ function packCoverage(cov, w, h, bpp) {
94
103
  const col = b * perByte + s;
95
104
  if (col >= w) continue;
96
105
  const c = cov[row * w + col];
97
- const v = bpp === 1 ? (c >= 128 ? 1 : 0) : Math.round((c * maxVal) / 255);
106
+ const v =
107
+ bpp === 1 ? (c >= 128 ? 1 : 0) : Math.round((c * maxVal) / 255);
98
108
  byte |= v << (8 - bpp - s * bpp); // first pixel in the high bits
99
109
  }
100
110
  out.push(byte);
@@ -109,15 +119,18 @@ function packCoverage(cov, w, h, bpp) {
109
119
  * @returns {{glyph:object, bytes:number[]}|null} null when the font has no glyph for the codepoint.
110
120
  */
111
121
  function bakeGlyph(font, cp, pixelSize, baseline, scale, bpp) {
112
- if (font.charToGlyphIndex(String.fromCodePoint(cp)) === 0 && cp !== 0x20) return null;
122
+ if (font.charToGlyphIndex(String.fromCodePoint(cp)) === 0 && cp !== 0x20)
123
+ return null;
113
124
  const glyph = font.charToGlyph(String.fromCodePoint(cp));
114
125
  const advance = Math.round(glyph.advanceWidth * scale);
115
126
  const ss = bpp === 8 ? 8 : 4;
116
127
  const steps = Math.max(6, Math.ceil(pixelSize / 3));
117
- const r = rasterize(glyph.getPath(0, baseline, pixelSize), { ss, steps });
128
+ const r = rasterize(glyph.getPath(0, baseline, pixelSize), {ss, steps});
118
129
 
119
130
  if (r.width > 255 || r.height > 255) {
120
- throw new Error(`glyph U+${cp.toString(16)} is ${r.width}x${r.height}px — exceeds the uint8 glyph limit`);
131
+ throw new Error(
132
+ `glyph U+${cp.toString(16)} is ${r.width}x${r.height}px — exceeds the uint8 glyph limit`,
133
+ );
121
134
  }
122
135
  const glyphRec = {
123
136
  bitmapOffset: 0, // filled in by the caller once the running offset is known
@@ -127,8 +140,11 @@ function bakeGlyph(font, cp, pixelSize, baseline, scale, bpp) {
127
140
  yOffset: r.yOffset,
128
141
  advance: Math.min(advance, 255),
129
142
  };
130
- const bytes = r.width > 0 && r.height > 0 ? packCoverage(r.coverage, r.width, r.height, bpp) : [];
131
- return { glyph: glyphRec, bytes };
143
+ const bytes =
144
+ r.width > 0 && r.height > 0
145
+ ? packCoverage(r.coverage, r.width, r.height, bpp)
146
+ : [];
147
+ return {glyph: glyphRec, bytes};
132
148
  }
133
149
 
134
150
  /**
@@ -142,8 +158,9 @@ function bakeGlyph(font, cp, pixelSize, baseline, scale, bpp) {
142
158
  * @param {string|Array<number>} [opts.glyphs] Extra glyph coverage beyond ASCII (default 'ascii').
143
159
  * @returns {{family:string, sizes:Array<object>}} Baked data ready for the C emitter.
144
160
  */
145
- export function bakeFont({ path, family, sizes, bpp = 4, glyphs = 'ascii' }) {
146
- if (![1, 2, 4, 8].includes(bpp)) throw new Error(`bpp must be 1, 2, 4, or 8 (got ${bpp})`);
161
+ export function bakeFont({path, family, sizes, bpp = 4, glyphs = 'ascii'}) {
162
+ if (![1, 2, 4, 8].includes(bpp))
163
+ throw new Error(`bpp must be 1, 2, 4, or 8 (got ${bpp})`);
147
164
  const font = opentype.parse(fs.readFileSync(path).buffer);
148
165
  const extraCps = resolveExtras(glyphs);
149
166
 
@@ -157,7 +174,14 @@ export function bakeFont({ path, family, sizes, bpp = 4, glyphs = 'ascii' }) {
157
174
  const dense = [];
158
175
  for (let cp = ASCII_FIRST; cp <= ASCII_LAST; cp++) {
159
176
  const baked1 = bakeGlyph(font, cp, pixelSize, baseline, scale, bpp) || {
160
- glyph: { bitmapOffset: 0, width: 0, height: 0, xOffset: 0, yOffset: 0, advance: Math.round(pixelSize / 2) },
177
+ glyph: {
178
+ bitmapOffset: 0,
179
+ width: 0,
180
+ height: 0,
181
+ xOffset: 0,
182
+ yOffset: 0,
183
+ advance: Math.round(pixelSize / 2),
184
+ },
161
185
  bytes: [],
162
186
  };
163
187
  baked1.glyph.bitmapOffset = baked1.bytes.length > 0 ? bitmap.length : 0;
@@ -171,7 +195,7 @@ export function bakeFont({ path, family, sizes, bpp = 4, glyphs = 'ascii' }) {
171
195
  if (!b) continue; // font has no such glyph
172
196
  b.glyph.bitmapOffset = b.bytes.length > 0 ? bitmap.length : 0;
173
197
  bitmap.push(...b.bytes);
174
- extras.push({ codepoint: cp, info: b.glyph });
198
+ extras.push({codepoint: cp, info: b.glyph});
175
199
  }
176
200
 
177
201
  baked.push({
@@ -186,5 +210,5 @@ export function bakeFont({ path, family, sizes, bpp = 4, glyphs = 'ascii' }) {
186
210
  bitmap,
187
211
  });
188
212
  }
189
- return { family, sizes: baked };
213
+ return {family, sizes: baked};
190
214
  }
@@ -18,7 +18,7 @@
18
18
  // references by pointer (er_image_load). Pure JS (pngjs) — no native deps, no Python. The engine
19
19
  // scales at render time, so bake at whatever source resolution you want to ship.
20
20
  import fs from 'node:fs';
21
- import { PNG } from 'pngjs';
21
+ import {PNG} from 'pngjs';
22
22
 
23
23
  /**
24
24
  * Decodes an image and returns its premultiplied ARGB8888 pixels (row-major, 0xAARRGGBB).
@@ -28,12 +28,14 @@ import { PNG } from 'pngjs';
28
28
  * @param {string} opts.name Asset name an <Image source>/imageName looks up.
29
29
  * @returns {{name:string, width:number, height:number, pixels:Uint32Array}}
30
30
  */
31
- export function bakeImage({ path, name }) {
31
+ export function bakeImage({path, name}) {
32
32
  if (!/\.png$/i.test(path)) {
33
- throw new Error(`image "${path}": only PNG is supported by the baker (convert to PNG, or extend bake-image.mjs)`);
33
+ throw new Error(
34
+ `image "${path}": only PNG is supported by the baker (convert to PNG, or extend bake-image.mjs)`,
35
+ );
34
36
  }
35
37
  const png = PNG.sync.read(fs.readFileSync(path));
36
- const { width, height, data } = png; // data = RGBA, 8-bit, row-major
38
+ const {width, height, data} = png; // data = RGBA, 8-bit, row-major
37
39
  const pixels = new Uint32Array(width * height);
38
40
  for (let i = 0; i < width * height; i++) {
39
41
  const r = data[i * 4];
@@ -46,5 +48,5 @@ export function bakeImage({ path, name }) {
46
48
  const bp = Math.floor((b * a + 127) / 255);
47
49
  pixels[i] = ((a << 24) | (rp << 16) | (gp << 8) | bp) >>> 0;
48
50
  }
49
- return { name, width, height, pixels };
51
+ return {name, width, height, pixels};
50
52
  }