numbl 0.4.4 → 0.4.5

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Numbl
2
2
 
3
- A MATLAB-compatible numerical computing environment with 400+ built-in functions. Runs in your browser or on the command line.
3
+ A numerical computing environment compatible with MATLAB syntax, featuring 400+ built-in functions. Runs in your browser or on the command line.
4
4
 
5
5
  [![numbl REPL](docs/repl-preview.svg)](https://numbl.org/embed-repl)
6
6
 
@@ -61,3 +61,7 @@ Jeremy Magland and Dan Fortunato, Center for Computational Mathematics, Flatiron
61
61
  ## License
62
62
 
63
63
  Apache 2.0
64
+
65
+ ## Disclaimer
66
+
67
+ numbl is not affiliated with, endorsed by, or supported by MathWorks, Inc. MATLAB is a registered trademark of MathWorks, Inc.
package/dist-cli/cli.js CHANGED
@@ -347,8 +347,33 @@ Expected directory: ${distDir}`
347
347
  const sseClients = [];
348
348
  let messageId = 0;
349
349
  const pendingMessages = [];
350
+ let uihtmlHandler;
350
351
  const server = createServer((req, res) => {
351
352
  const url2 = new URL(req.url ?? "/", `http://localhost`);
353
+ if (req.method === "POST" && url2.pathname === "/uihtml-event") {
354
+ const chunks = [];
355
+ let size2 = 0;
356
+ let aborted = false;
357
+ req.on("data", (chunk) => {
358
+ size2 += chunk.length;
359
+ if (size2 > 5e6) {
360
+ aborted = true;
361
+ req.destroy();
362
+ } else {
363
+ chunks.push(chunk);
364
+ }
365
+ });
366
+ req.on("end", () => {
367
+ if (aborted) return;
368
+ try {
369
+ uihtmlHandler?.(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
370
+ } catch {
371
+ }
372
+ res.writeHead(204);
373
+ res.end();
374
+ });
375
+ return;
376
+ }
352
377
  if (url2.pathname === "/events") {
353
378
  res.writeHead(200, {
354
379
  "Content-Type": "text/event-stream",
@@ -411,12 +436,7 @@ Expected directory: ${distDir}`
411
436
  };
412
437
  process.on("SIGINT", onExit);
413
438
  process.on("SIGTERM", onExit);
414
- function sendInstructions(instructions) {
415
- messageId++;
416
- const payload = `id: ${messageId}
417
- data: ${JSON.stringify(instructions)}
418
-
419
- `;
439
+ function broadcast(payload) {
420
440
  if (sseClients.length === 0) {
421
441
  pendingMessages.push(payload);
422
442
  } else {
@@ -425,22 +445,41 @@ data: ${JSON.stringify(instructions)}
425
445
  }
426
446
  }
427
447
  }
448
+ function sendInstructions(instructions) {
449
+ messageId++;
450
+ broadcast(`id: ${messageId}
451
+ data: ${JSON.stringify(instructions)}
452
+
453
+ `);
454
+ }
455
+ function sendUihtmlEvent(compId, name, dataJson) {
456
+ messageId++;
457
+ const data = JSON.stringify({ compId, name, dataJson });
458
+ broadcast(`id: ${messageId}
459
+ event: uihtml
460
+ data: ${data}
461
+
462
+ `);
463
+ }
464
+ function setUihtmlEventHandler(fn) {
465
+ uihtmlHandler = fn;
466
+ }
428
467
  function scriptDone() {
429
468
  messageId++;
430
- const payload = `id: ${messageId}
469
+ broadcast(`id: ${messageId}
431
470
  event: done
432
471
  data: {}
433
472
 
434
- `;
435
- if (sseClients.length === 0) {
436
- pendingMessages.push(payload);
437
- } else {
438
- for (const client of sseClients) {
439
- client.write(payload);
440
- }
441
- }
473
+ `);
442
474
  }
443
- return { sendInstructions, scriptDone, port, closed };
475
+ return {
476
+ sendInstructions,
477
+ sendUihtmlEvent,
478
+ setUihtmlEventHandler,
479
+ scriptDone,
480
+ port,
481
+ closed
482
+ };
444
483
  }
445
484
  function openBrowser(url) {
446
485
  const platform = process.platform;
@@ -465,12 +504,34 @@ function createPlotHandler(disabled, plotOpts) {
465
504
  return {
466
505
  onDrawnow: void 0,
467
506
  flushAndWait: async () => {
507
+ },
508
+ sendUihtmlEvent: () => {
509
+ },
510
+ setUihtmlSession: () => {
468
511
  }
469
512
  };
470
513
  }
471
514
  let plotServer = null;
472
515
  let serverStarting = null;
473
516
  const pendingBatches = [];
517
+ let session = null;
518
+ const wireServer = (ps) => {
519
+ ps.setUihtmlEventHandler((e) => {
520
+ if (!session) return;
521
+ const eventType = e.kind === "dataChanged" ? "DataChanged" : "HTMLEventReceived";
522
+ try {
523
+ session.dispatchEvent(e.compId, eventType, {
524
+ name: e.name,
525
+ data: e.data
526
+ });
527
+ } catch (err) {
528
+ process.stderr.write(
529
+ `Error in uihtml callback: ${err instanceof Error ? err.message : String(err)}
530
+ `
531
+ );
532
+ }
533
+ });
534
+ };
474
535
  const onDrawnow = (instructions) => {
475
536
  if (plotServer) {
476
537
  plotServer.sendInstructions(instructions);
@@ -479,6 +540,7 @@ function createPlotHandler(disabled, plotOpts) {
479
540
  if (!serverStarting) {
480
541
  serverStarting = startPlotServer(plotOpts).then((ps) => {
481
542
  plotServer = ps;
543
+ wireServer(ps);
482
544
  for (const batch of pendingBatches) {
483
545
  ps.sendInstructions(batch);
484
546
  }
@@ -500,7 +562,13 @@ function createPlotHandler(disabled, plotOpts) {
500
562
  await plotServer.closed;
501
563
  }
502
564
  };
503
- return { onDrawnow, flushAndWait };
565
+ const sendUihtmlEvent = (compId, name, dataJson) => {
566
+ plotServer?.sendUihtmlEvent(compId, name, dataJson);
567
+ };
568
+ const setUihtmlSession = (s) => {
569
+ session = s;
570
+ };
571
+ return { onDrawnow, flushAndWait, sendUihtmlEvent, setUihtmlSession };
504
572
  }
505
573
 
506
574
  // src/numbl-core/runtime/refcount.ts
@@ -34494,7 +34562,15 @@ function dispatchPlotBuiltin(name, args, instructions, state) {
34494
34562
  return true;
34495
34563
  // ── Graphics ops: figure / labels / hold / layout ──────────────
34496
34564
  case "figure": {
34497
- const handle = args.length > 0 ? args[0] : 1;
34565
+ let handle;
34566
+ if (args.length > 0) {
34567
+ handle = toNumber(args[0]);
34568
+ state.maxFigureHandle = Math.max(state.maxFigureHandle ?? 0, handle);
34569
+ } else {
34570
+ handle = (state.maxFigureHandle ?? 0) + 1;
34571
+ state.maxFigureHandle = handle;
34572
+ }
34573
+ state.currentFigureHandle = handle;
34498
34574
  plotInstr(instructions, { type: "set_figure_handle", handle });
34499
34575
  return true;
34500
34576
  }
@@ -38386,6 +38462,10 @@ var SPECIAL_BUILTIN_NAMES = [
38386
38462
  "stream2",
38387
38463
  "ishold",
38388
38464
  "figure",
38465
+ "drawuihtml",
38466
+ "registeruihtmlcallback",
38467
+ "sendEventToHTMLSource",
38468
+ "uigridlayout",
38389
38469
  "subplot",
38390
38470
  "tiledlayout",
38391
38471
  "nexttile",
@@ -39735,15 +39815,68 @@ function registerSpecialBuiltins(rt) {
39735
39815
  return rt.ishold();
39736
39816
  });
39737
39817
  registerSpecial("figure", (nargout, args) => {
39738
- const handle = args.length > 0 ? args[0] : 1;
39739
39818
  dispatchPlotBuiltin(
39740
39819
  "figure",
39741
39820
  args.map(ensureRuntimeValue),
39742
39821
  rt.plotInstructions,
39743
39822
  rt
39744
39823
  );
39745
- return nargout >= 1 ? RTV.num(toNumber(ensureRuntimeValue(handle))) : void 0;
39824
+ return nargout >= 1 ? RTV.num(rt.currentFigureHandle) : void 0;
39746
39825
  });
39826
+ let uihtmlSeq = 0;
39827
+ registerSpecial("drawuihtml", (nargout, args) => {
39828
+ const html = args.length > 0 ? toString(ensureRuntimeValue(args[0])) : "";
39829
+ const data = args.length > 1 ? toString(ensureRuntimeValue(args[1])) : void 0;
39830
+ uihtmlSeq += 1;
39831
+ const id = "uh" + uihtmlSeq;
39832
+ rt.plotInstructions.push({
39833
+ type: "uihtml",
39834
+ id,
39835
+ html,
39836
+ ...data !== void 0 ? { data } : {}
39837
+ });
39838
+ return nargout >= 1 ? RTV.char(id) : void 0;
39839
+ });
39840
+ registerSpecial("registeruihtmlcallback", (_nargout, args) => {
39841
+ if (args.length < 3) return void 0;
39842
+ const compId = toString(ensureRuntimeValue(args[0]));
39843
+ const eventType = toString(ensureRuntimeValue(args[1]));
39844
+ const fcn = ensureRuntimeValue(args[2]);
39845
+ if (eventType !== "HTMLEventReceived" && eventType !== "DataChanged") {
39846
+ return void 0;
39847
+ }
39848
+ if (!isRuntimeFunction(fcn)) return void 0;
39849
+ const entry = rt.uihtmlCallbacks.get(compId) ?? {};
39850
+ const prev = entry[eventType];
39851
+ if (prev) decref(rt, prev);
39852
+ incref(fcn);
39853
+ entry[eventType] = fcn;
39854
+ rt.uihtmlCallbacks.set(compId, entry);
39855
+ return void 0;
39856
+ });
39857
+ registerSpecial("sendEventToHTMLSource", (_nargout, args) => {
39858
+ if (args.length < 2) {
39859
+ throw new RuntimeError("sendEventToHTMLSource requires src and name");
39860
+ }
39861
+ const src = ensureRuntimeValue(args[0]);
39862
+ if (!isRuntimeStruct(src) || !src.fields.has("ComponentId")) {
39863
+ throw new RuntimeError(
39864
+ "sendEventToHTMLSource: src must be the component passed to the callback"
39865
+ );
39866
+ }
39867
+ const compId = toString(src.fields.get("ComponentId"));
39868
+ const name = toString(ensureRuntimeValue(args[1]));
39869
+ const dataArg = args.length > 2 ? ensureRuntimeValue(args[2]) : RTV.num(0);
39870
+ const dataJson = toString(
39871
+ ensureRuntimeValue(rt.dispatch("jsonencode", 1, [dataArg]))
39872
+ );
39873
+ rt.onHtmlSourceEvent?.(compId, name, dataJson);
39874
+ return void 0;
39875
+ });
39876
+ registerSpecial(
39877
+ "uigridlayout",
39878
+ (nargout) => nargout >= 1 ? RTV.num(0) : void 0
39879
+ );
39747
39880
  const PLOT_RETURNS_ZERO = ["subplot", "tiledlayout", "nexttile", "legend"];
39748
39881
  for (const name of PLOT_RETURNS_ZERO) {
39749
39882
  registerSpecial(name, (nargout, args) => {
@@ -41159,6 +41292,19 @@ var figuresReducer = (state, action) => {
41159
41292
  switch (action.type) {
41160
41293
  case "set_figure_handle":
41161
41294
  return { ...state, currentHandle: action.handle };
41295
+ case "uihtml": {
41296
+ const fig = ensureFig(state);
41297
+ return {
41298
+ ...state,
41299
+ figs: {
41300
+ ...state.figs,
41301
+ [state.currentHandle]: {
41302
+ ...fig,
41303
+ uihtml: { id: action.id, html: action.html, data: action.data }
41304
+ }
41305
+ }
41306
+ };
41307
+ }
41162
41308
  case "set_hold":
41163
41309
  return updateAxes(state, { holdOn: action.value });
41164
41310
  case "plot": {
@@ -41599,6 +41745,7 @@ var Runtime = class _Runtime {
41599
41745
  this.profilingEnabled = !!options.profile;
41600
41746
  this.fileIO = options.fileIO;
41601
41747
  this.system = options.system;
41748
+ this.onHtmlSourceEvent = options.onHtmlSourceEvent;
41602
41749
  if (options.initialHoldState) {
41603
41750
  this.holdState = options.initialHoldState;
41604
41751
  }
@@ -41617,9 +41764,24 @@ var Runtime = class _Runtime {
41617
41764
  plotInstructions = [];
41618
41765
  variableValues = {};
41619
41766
  holdState = false;
41767
+ /** Figure-handle tracking for MATLAB-style `figure` allocation: `figure`
41768
+ * with no argument creates a new figure (`maxFigureHandle + 1`). */
41769
+ currentFigureHandle = 0;
41770
+ maxFigureHandle = 0;
41620
41771
  /** Monotonic id source for graphics handles whose trace can be live-updated
41621
41772
  * via `set` / `update_trace` (e.g. lines returned by `line`). */
41622
41773
  graphicsIdCounter = 1;
41774
+ /** Reverse channel for `uihtml` components (HTML → MATLAB). Maps a component
41775
+ * id (the `uihtml` instruction's `id`) to the callback handles registered
41776
+ * for it. When non-empty after a run, the host keeps this runtime alive so
41777
+ * iframe events can re-enter the interpreter and invoke these handles.
41778
+ * Handles are incref'd while stored (see registeruihtmlcallback). */
41779
+ uihtmlCallbacks = /* @__PURE__ */ new Map();
41780
+ /** Hook invoked by `sendEventToHTMLSource(src, name, data)` to push an event
41781
+ * from MATLAB back to a uihtml component's page. `dataJson` is the data
41782
+ * already `jsonencode`d. Set from ExecOptions; the host forwards it to the
41783
+ * iframe. */
41784
+ onHtmlSourceEvent;
41623
41785
  // tiledlayout/nexttile state. Reset by tiledlayout, advanced by nexttile.
41624
41786
  // mode: "fixed" uses the rows/cols verbatim; "flow"/"vertical"/"horizontal"
41625
41787
  // grow the grid as tiles are added.
@@ -41752,6 +41914,15 @@ var Runtime = class _Runtime {
41752
41914
  }
41753
41915
  }
41754
41916
  // ── Builtin initialization ──────────────────────────────────────────
41917
+ /** Register the implicit default figure (handle 1) the first time a graphics
41918
+ * op runs without an explicit `figure`, so a later no-arg `figure` allocates
41919
+ * a new handle instead of reusing 1 (MATLAB semantics). */
41920
+ ensureCurrentFigure() {
41921
+ if (!this.currentFigureHandle) {
41922
+ this.currentFigureHandle = 1;
41923
+ this.maxFigureHandle = Math.max(this.maxFigureHandle, 1);
41924
+ }
41925
+ }
41755
41926
  initBuiltins() {
41756
41927
  registerSpecialBuiltins(this);
41757
41928
  for (const name of PLOT_DISPATCH_NAMES) {
@@ -41897,6 +42068,22 @@ var Runtime = class _Runtime {
41897
42068
  this.builtins["stream2"] = (_nargout, args) => {
41898
42069
  return stream2Call(args.map(ensureRuntimeValue));
41899
42070
  };
42071
+ const NO_AUTO_FIGURE = /* @__PURE__ */ new Set(["figure", "close", "clf", "cla", "axis"]);
42072
+ for (const name of [
42073
+ ...PLOT_DISPATCH_NAMES,
42074
+ "fplot",
42075
+ "fplot3",
42076
+ "streamline",
42077
+ "stream2"
42078
+ ]) {
42079
+ if (NO_AUTO_FIGURE.has(name)) continue;
42080
+ const orig = this.builtins[name];
42081
+ if (!orig) continue;
42082
+ this.builtins[name] = (n, a) => {
42083
+ this.ensureCurrentFigure();
42084
+ return orig(n, a);
42085
+ };
42086
+ }
41900
42087
  }
41901
42088
  profileEnter(key) {
41902
42089
  if (!this.profilingEnabled) return;
@@ -54476,6 +54663,149 @@ for (const { name, min: min2, max: max2 } of INT_RANGES) {
54476
54663
  ]
54477
54664
  });
54478
54665
  }
54666
+ var TYPECAST_VIEWS = {
54667
+ uint8: (b) => new Uint8Array(b),
54668
+ int8: (b) => new Int8Array(b),
54669
+ uint16: (b) => new Uint16Array(b),
54670
+ int16: (b) => new Int16Array(b),
54671
+ uint32: (b) => new Uint32Array(b),
54672
+ int32: (b) => new Int32Array(b),
54673
+ single: (b) => new Float32Array(b),
54674
+ double: (b) => new Float64Array(b)
54675
+ };
54676
+ defineBuiltin({
54677
+ name: "typecast",
54678
+ help: {
54679
+ signatures: ["B = typecast(X, CLASS)"],
54680
+ description: "Reinterpret the raw bytes of numeric array X as CLASS (e.g. 'uint8', 'single', 'int32'). numbl stores all numerics as double, so X is treated as double-precision; this is mainly for serializing numeric data to bytes, e.g. typecast(double(x), 'uint8')."
54681
+ },
54682
+ cases: [
54683
+ {
54684
+ match: (argTypes) => {
54685
+ if (argTypes.length !== 2) return null;
54686
+ const a = argTypes[0];
54687
+ if (a.kind === "number" || a.kind === "boolean" || a.kind === "tensor" || a.kind === "complex_or_number")
54688
+ return [{ kind: "tensor", isComplex: false }];
54689
+ return null;
54690
+ },
54691
+ apply: (args) => {
54692
+ const X = args[0];
54693
+ const cls = toString(args[1]).toLowerCase();
54694
+ const makeView = TYPECAST_VIEWS[cls];
54695
+ if (!makeView)
54696
+ throw new RuntimeError(`typecast: unsupported class '${cls}'`);
54697
+ let src;
54698
+ if (isRuntimeNumber(X)) src = Float64Array.of(X);
54699
+ else if (isRuntimeLogical(X)) src = Float64Array.of(X ? 1 : 0);
54700
+ else if (isRuntimeTensor(X)) {
54701
+ if (X.imag)
54702
+ throw new RuntimeError("typecast: complex input not supported");
54703
+ src = X.data;
54704
+ } else {
54705
+ throw new RuntimeError("typecast: X must be a numeric array");
54706
+ }
54707
+ const buffer = src.buffer.slice(
54708
+ src.byteOffset,
54709
+ src.byteOffset + src.byteLength
54710
+ );
54711
+ const view = makeView(buffer);
54712
+ const out = allocFloat64Array(view.length);
54713
+ for (let i = 0; i < view.length; i++) out[i] = view[i];
54714
+ return RTV.tensor(out, [1, view.length]);
54715
+ }
54716
+ }
54717
+ ]
54718
+ });
54719
+ function jsonEncodeNumber(x) {
54720
+ return Number.isFinite(x) ? String(x) : "null";
54721
+ }
54722
+ function jsonEncodeTensor(v) {
54723
+ if (v.imag && !imagAllZero(v.imag))
54724
+ throw new RuntimeError("jsonencode: complex values are not supported");
54725
+ const data = v.data;
54726
+ const n = data.length;
54727
+ const enc = v._isLogical ? (x) => x ? "true" : "false" : jsonEncodeNumber;
54728
+ const shape = v.shape ?? [n];
54729
+ if (n === 1 && shape.every((s) => s === 1)) return enc(data[0]);
54730
+ if (n === 0) return "[]";
54731
+ const nonSingleton = shape.filter((s) => s > 1).length;
54732
+ if (shape.length <= 1 || nonSingleton <= 1) {
54733
+ const parts2 = [];
54734
+ for (let i = 0; i < n; i++) parts2.push(enc(data[i]));
54735
+ return "[" + parts2.join(",") + "]";
54736
+ }
54737
+ if (shape.length === 2) {
54738
+ const [m, cols] = shape;
54739
+ const rows = [];
54740
+ for (let i = 0; i < m; i++) {
54741
+ const rowParts = [];
54742
+ for (let j = 0; j < cols; j++) rowParts.push(enc(data[j * m + i]));
54743
+ rows.push("[" + rowParts.join(",") + "]");
54744
+ }
54745
+ return "[" + rows.join(",") + "]";
54746
+ }
54747
+ const parts = [];
54748
+ for (let i = 0; i < n; i++) parts.push(enc(data[i]));
54749
+ return "[" + parts.join(",") + "]";
54750
+ }
54751
+ function jsonEncodeCell(v) {
54752
+ if (v.data.length === 0) return "[]";
54753
+ return "[" + v.data.map(jsonEncodeValue).join(",") + "]";
54754
+ }
54755
+ function jsonEncodeStruct(v) {
54756
+ const parts = [];
54757
+ for (const [key, val] of v.fields) {
54758
+ parts.push(JSON.stringify(key) + ":" + jsonEncodeValue(val));
54759
+ }
54760
+ return "{" + parts.join(",") + "}";
54761
+ }
54762
+ function jsonEncodeValue(v) {
54763
+ if (v === void 0 || v === null) return "null";
54764
+ if (isRuntimeNumber(v)) return jsonEncodeNumber(v);
54765
+ if (isRuntimeLogical(v)) return v ? "true" : "false";
54766
+ if (isRuntimeString(v)) return JSON.stringify(v);
54767
+ if (isRuntimeChar(v)) {
54768
+ if (v.shape && v.shape.length === 2 && v.shape[0] > 1) {
54769
+ const rows = v.shape[0];
54770
+ const cols = v.shape[1];
54771
+ const out = [];
54772
+ for (let i = 0; i < rows; i++) {
54773
+ let row = "";
54774
+ for (let j = 0; j < cols; j++) row += v.value[j * rows + i] ?? "";
54775
+ out.push(JSON.stringify(row));
54776
+ }
54777
+ return "[" + out.join(",") + "]";
54778
+ }
54779
+ return JSON.stringify(v.value);
54780
+ }
54781
+ if (isRuntimeComplexNumber(v)) {
54782
+ if (v.im !== 0)
54783
+ throw new RuntimeError("jsonencode: complex values are not supported");
54784
+ return jsonEncodeNumber(v.re);
54785
+ }
54786
+ if (isRuntimeTensor(v)) return jsonEncodeTensor(v);
54787
+ if (isRuntimeCell(v)) return jsonEncodeCell(v);
54788
+ if (isRuntimeStruct(v)) return jsonEncodeStruct(v);
54789
+ if (isRuntimeStructArray(v))
54790
+ return "[" + v.elements.map(jsonEncodeStruct).join(",") + "]";
54791
+ throw new RuntimeError("jsonencode: unsupported value type");
54792
+ }
54793
+ defineBuiltin({
54794
+ name: "jsonencode",
54795
+ help: {
54796
+ signatures: ["txt = jsonencode(V)"],
54797
+ description: "Encode value V (struct, cell, char/string, logical, or numeric array) as a JSON-formatted char row vector."
54798
+ },
54799
+ cases: [
54800
+ {
54801
+ match: (argTypes) => {
54802
+ if (argTypes.length < 1) return null;
54803
+ return [{ kind: "char" }];
54804
+ },
54805
+ apply: (args) => RTV.char(jsonEncodeValue(args[0]))
54806
+ }
54807
+ ]
54808
+ });
54479
54809
  function idivideMode(args) {
54480
54810
  if (args.length < 3) return "fix";
54481
54811
  const m = args[2];
@@ -58942,7 +59272,7 @@ function getSourceLine(getSource, file, line) {
58942
59272
  }
58943
59273
 
58944
59274
  // src/numbl-core/version.ts
58945
- var NUMBL_VERSION = "0.4.4";
59275
+ var NUMBL_VERSION = "0.4.5";
58946
59276
 
58947
59277
  // src/cli-repl.ts
58948
59278
  import { createInterface } from "readline";
@@ -64361,6 +64691,10 @@ end
64361
64691
  {
64362
64692
  name: "isIllConditioned.m",
64363
64693
  source: "function result = isIllConditioned(dA)\n result = dA.isIllCond;\nend\n"
64694
+ },
64695
+ {
64696
+ name: "uihtml.m",
64697
+ source: "classdef uihtml < handle\n %UIHTML Create an HTML UI component (numbl subset).\n % H = UIHTML('HTMLSource', HTML) renders the self-contained HTML string\n % HTML in the figure pane. An optional leading parent argument (e.g. a\n % figure) is accepted and ignored. Supported name-value options:\n % HTMLSource, Data, Position.\n %\n % H = UIHTML('HTMLSource', HTML, 'Data', X) also sends X into the page.\n % Mirroring MATLAB, X is encoded with jsonencode, parsed in the page with\n % JSON.parse, and set on the JavaScript `htmlComponent.Data` object,\n % firing any \"DataChanged\" listener registered in the page's\n % `function setup(htmlComponent)`. To update the data after construction,\n % set H.Data and call show(H) (numbl re-renders the component):\n %\n % h = uihtml('HTMLSource', html, 'Data', struct('n', 1));\n % h.Data = struct('n', 2);\n % show(h);\n %\n % numbl currently supports HTMLSource given as an HTML markup string (a\n % single self-contained document). HTML file paths and supporting files\n % are not yet supported.\n %\n % The reverse channel (page -> MATLAB) is supported for the IDE:\n % HTMLEventReceivedFcn fires when JS calls\n % htmlComponent.sendEventToMATLAB(name,data); inside the callback use\n % sendEventToHTMLSource(src,name,data) to send back to the page.\n % DataChangedFcn fires when JS sets htmlComponent.Data. Register callbacks\n % at construction (name-value) since numbl renders at construction.\n properties\n HTMLSource = ''\n Data = []\n Position = [100 100 100 100]\n HTMLEventReceivedFcn = []\n DataChangedFcn = []\n end\n methods\n function obj = uihtml(varargin)\n args = varargin;\n % Ignore an optional leading parent argument (anything that is not\n % a name-value name string).\n if numel(args) >= 1 && ~(ischar(args{1}) || isstring(args{1}))\n args = args(2:end);\n end\n for i = 1:2:numel(args) - 1\n name = args{i};\n val = args{i + 1};\n if strcmpi(name, 'HTMLSource')\n obj.HTMLSource = val;\n elseif strcmpi(name, 'Data')\n obj.Data = val;\n elseif strcmpi(name, 'Position')\n obj.Position = val;\n elseif strcmpi(name, 'HTMLEventReceivedFcn')\n obj.HTMLEventReceivedFcn = val;\n elseif strcmpi(name, 'DataChangedFcn')\n obj.DataChangedFcn = val;\n end\n end\n render(obj);\n end\n\n function show(obj)\n render(obj);\n end\n\n function render(obj)\n if isempty(obj.HTMLSource)\n return;\n end\n if isempty(obj.Data)\n id = drawuihtml(obj.HTMLSource);\n else\n id = drawuihtml(obj.HTMLSource, jsonencode(obj.Data));\n end\n if ~isempty(obj.HTMLEventReceivedFcn)\n registeruihtmlcallback(id, 'HTMLEventReceived', ...\n obj.HTMLEventReceivedFcn);\n end\n if ~isempty(obj.DataChangedFcn)\n registeruihtmlcallback(id, 'DataChanged', obj.DataChangedFcn);\n end\n end\n end\nend\n"
64364
64698
  }
64365
64699
  ];
64366
64700
 
@@ -92326,6 +92660,64 @@ function registerExecutorsForOpt(registry3, opt) {
92326
92660
  }
92327
92661
  }
92328
92662
 
92663
+ // src/numbl-core/runtime/uihtmlSession.ts
92664
+ function createUihtmlSession(rt, activeSpecials, onDrawnow) {
92665
+ function invokeHandle(fn, args) {
92666
+ if (fn.jsFn) {
92667
+ if (fn.jsFnExpectsNargout) fn.jsFn(0, ...args);
92668
+ else fn.jsFn(...args);
92669
+ return;
92670
+ }
92671
+ rt.dispatch(fn.name, 0, args);
92672
+ }
92673
+ function dispatchEvent(compId, eventType, payload) {
92674
+ const entry = rt.uihtmlCallbacks.get(compId);
92675
+ const fn = entry?.[eventType];
92676
+ if (!fn || !isRuntimeFunction(fn)) return;
92677
+ const saved = /* @__PURE__ */ new Map();
92678
+ for (const name of SPECIAL_BUILTIN_NAMES) {
92679
+ const ex = getIBuiltin(name);
92680
+ if (ex) saved.set(name, ex);
92681
+ }
92682
+ for (const ib of activeSpecials.values()) registerDynamicIBuiltin(ib);
92683
+ pushCurrentRuntime(rt);
92684
+ const before = rt.plotInstructions.length;
92685
+ try {
92686
+ const src = RTV.struct({ ComponentId: RTV.char(compId) });
92687
+ const dataRV = convertJsonValue(payload.data);
92688
+ const event = eventType === "HTMLEventReceived" ? RTV.struct({
92689
+ HTMLEventName: RTV.char(payload.name ?? ""),
92690
+ HTMLEventData: dataRV,
92691
+ Source: src,
92692
+ EventName: RTV.char("HTMLEventReceived")
92693
+ }) : RTV.struct({
92694
+ Data: dataRV,
92695
+ PreviousData: RTV.tensor(new Float64Array(0), [0, 0]),
92696
+ Source: src,
92697
+ EventName: RTV.char("DataChanged")
92698
+ });
92699
+ invokeHandle(fn, [src, event]);
92700
+ } finally {
92701
+ popCurrentRuntime(rt);
92702
+ for (const ib of saved.values()) registerDynamicIBuiltin(ib);
92703
+ }
92704
+ const newInstrs = rt.plotInstructions.slice(before);
92705
+ if (newInstrs.length && onDrawnow) onDrawnow(newInstrs);
92706
+ }
92707
+ function dispose() {
92708
+ for (const entry of rt.uihtmlCallbacks.values()) {
92709
+ if (entry.HTMLEventReceived) decref(rt, entry.HTMLEventReceived);
92710
+ if (entry.DataChanged) decref(rt, entry.DataChanged);
92711
+ }
92712
+ rt.uihtmlCallbacks.clear();
92713
+ }
92714
+ return {
92715
+ hasCallbacks: () => rt.uihtmlCallbacks.size > 0,
92716
+ dispatchEvent,
92717
+ dispose
92718
+ };
92719
+ }
92720
+
92329
92721
  // src/numbl-core/executeCode.ts
92330
92722
  globalThis.FloatXArray = Float64Array;
92331
92723
  var SHIM_SEARCH_PATH = "__numbl_shims__";
@@ -92777,6 +93169,18 @@ ${sections.join("\n\n")}` : `// No ${label} generated`;
92777
93169
  variableValues: interpreter.getVariableValues(),
92778
93170
  holdState: rt.holdState
92779
93171
  };
93172
+ if (rt.uihtmlCallbacks.size > 0) {
93173
+ const activeSpecials = /* @__PURE__ */ new Map();
93174
+ for (const name of SPECIAL_BUILTIN_NAMES) {
93175
+ const ex = getIBuiltin(name);
93176
+ if (ex) activeSpecials.set(name, ex);
93177
+ }
93178
+ result.uihtmlSession = createUihtmlSession(
93179
+ rt,
93180
+ activeSpecials,
93181
+ options.onDrawnow
93182
+ );
93183
+ }
92780
93184
  if (options.profile) {
92781
93185
  result.profileData = {
92782
93186
  executionTimeMs,
@@ -94717,9 +95121,7 @@ ${code2}
94717
95121
  process.exit(0);
94718
95122
  } else if (opts.verbose) {
94719
95123
  const log3 = (msg) => console.error(`[verbose] ${msg}`);
94720
- const { onDrawnow, flushAndWait } = createPlotHandler(
94721
- !opts.plot || opts.stream
94722
- );
95124
+ const plot = createPlotHandler(!opts.plot || opts.stream);
94723
95125
  const result = executeCode(
94724
95126
  code,
94725
95127
  {
@@ -94728,7 +95130,8 @@ ${code2}
94728
95130
  onOutput: (text) => {
94729
95131
  process.stdout.write(text);
94730
95132
  },
94731
- onDrawnow,
95133
+ onDrawnow: plot.onDrawnow,
95134
+ onHtmlSourceEvent: plot.sendUihtmlEvent,
94732
95135
  log: log3,
94733
95136
  onJitCompile,
94734
95137
  onJitBail,
@@ -94744,13 +95147,11 @@ ${code2}
94744
95147
  );
94745
95148
  writeProfileIfNeeded(result);
94746
95149
  finalizeDumps();
94747
- await flushAndWait(result.plotInstructions);
95150
+ plot.setUihtmlSession(result.uihtmlSession ?? null);
95151
+ await plot.flushAndWait(result.plotInstructions);
94748
95152
  } else {
94749
95153
  const asyncPlotOpts = opts.plotPort !== void 0 ? { port: opts.plotPort, host: "0.0.0.0" } : void 0;
94750
- const { onDrawnow, flushAndWait } = createPlotHandler(
94751
- !opts.plot,
94752
- asyncPlotOpts
94753
- );
95154
+ const plot = createPlotHandler(!opts.plot, asyncPlotOpts);
94754
95155
  const result = executeCode(
94755
95156
  code,
94756
95157
  {
@@ -94759,7 +95160,8 @@ ${code2}
94759
95160
  onOutput: (text) => {
94760
95161
  process.stdout.write(text);
94761
95162
  },
94762
- onDrawnow,
95163
+ onDrawnow: plot.onDrawnow,
95164
+ onHtmlSourceEvent: plot.sendUihtmlEvent,
94763
95165
  onJitCompile,
94764
95166
  onJitBail,
94765
95167
  fileIO,
@@ -94774,7 +95176,8 @@ ${code2}
94774
95176
  );
94775
95177
  writeProfileIfNeeded(result);
94776
95178
  finalizeDumps();
94777
- await flushAndWait(result.plotInstructions);
95179
+ plot.setUihtmlSession(result.uihtmlSession ?? null);
95180
+ await plot.flushAndWait(result.plotInstructions);
94778
95181
  }
94779
95182
  process.exit(0);
94780
95183
  } catch (error) {
@@ -62,6 +62,15 @@ export type FigureState = {
62
62
  axes: {
63
63
  [index: number]: AxesState;
64
64
  };
65
+ /** When set, this figure is an HTML UI component (MATLAB `uihtml`): the
66
+ * `html` string is rendered in an iframe instead of the axes/trace canvas.
67
+ * Takes precedence over `axes`. `data` is the JSON-encoded `Data` property
68
+ * (from `jsonencode`), pushed into the page's `htmlComponent`. */
69
+ uihtml?: {
70
+ id: string;
71
+ html: string;
72
+ data?: string;
73
+ };
65
74
  };
66
75
  export type FiguresState = {
67
76
  currentHandle: number;