@yuneta/gobj-ui 7.23.19 → 7.23.21

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.
@@ -9346,6 +9346,14 @@ function yui_toolbar(attrs = {}, items = []) {
9346
9346
  }
9347
9347
  //#endregion
9348
9348
  //#region src/lib_graph.js
9349
+ /***********************************************************************
9350
+ * lib_graph.js
9351
+ *
9352
+ * Utilities for graph, using Bulma
9353
+ *
9354
+ * Copyright (c) 2025, ArtGins.
9355
+ * All Rights Reserved.
9356
+ ***********************************************************************/
9349
9357
  /************************************************************
9350
9358
  * Add class to elements selected
9351
9359
  ************************************************************/
@@ -9461,6 +9469,23 @@ function getStrokeColor(fillColor, theme = "light", factor = .2) {
9461
9469
  const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
9462
9470
  return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
9463
9471
  }
9472
+ /************************************************************
9473
+ * Returns the proportional position (between 0 and 1) of a specific point,
9474
+ * centered and spaced with margins.
9475
+ *
9476
+ * index - Index of the point (0 to count-1)
9477
+ * count - Total number of points
9478
+ * margin - Total margin space (default 0.2 means 10% on each end)
9479
+ ************************************************************/
9480
+ function getPointPosition(count, index, margin = .2) {
9481
+ if (count <= 0 || index < 0 || index >= count) {
9482
+ log_error("Invalid count or index");
9483
+ return .5;
9484
+ }
9485
+ const start = margin / 2;
9486
+ const step = (1 - margin / 2 - start) / count;
9487
+ return start + index * step + step / 2;
9488
+ }
9464
9489
  //#endregion
9465
9490
  //#region src/yui_graph_camera.js
9466
9491
  /***********************************************************************
@@ -10159,6 +10184,268 @@ function ensure_drag_canvas_patch() {
10159
10184
  register(ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
10160
10185
  __drag_canvas_patched__ = true;
10161
10186
  }
10187
+ /************************************************************
10188
+ * JSON type discriminator (viewer vocabulary)
10189
+ ************************************************************/
10190
+ function json_type(value) {
10191
+ if (value === null) return "null";
10192
+ if (Array.isArray(value)) return "array";
10193
+ switch (typeof value) {
10194
+ case "string": return "string";
10195
+ case "number": return "number";
10196
+ case "boolean": return "boolean";
10197
+ case "object": return "object";
10198
+ default: return "unknown";
10199
+ }
10200
+ }
10201
+ /************************************************************
10202
+ * Detect a kw_collapse() sentinel.
10203
+ *
10204
+ * The kernel replaces an over-limit dict with:
10205
+ * { "__collapsed__": { "path": ..., "size": N } }
10206
+ * and an over-limit array with:
10207
+ * [ { "__collapsed__": { "path": ..., "size": N } } ]
10208
+ *
10209
+ * Returns { size, path, is_array } or null.
10210
+ ************************************************************/
10211
+ function is_collapsed(value) {
10212
+ if (value && typeof value === "object" && !Array.isArray(value)) {
10213
+ let keys = Object.keys(value);
10214
+ if (keys.length === 1 && keys[0] === "__collapsed__") {
10215
+ let c = value.__collapsed__ || {};
10216
+ return {
10217
+ size: c.size,
10218
+ path: c.path,
10219
+ is_array: false
10220
+ };
10221
+ }
10222
+ return null;
10223
+ }
10224
+ if (Array.isArray(value) && value.length === 1) {
10225
+ let e = value[0];
10226
+ if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
10227
+ let c = e.__collapsed__ || {};
10228
+ return {
10229
+ size: c.size,
10230
+ path: c.path,
10231
+ is_array: true
10232
+ };
10233
+ }
10234
+ }
10235
+ return null;
10236
+ }
10237
+ /************************************************************
10238
+ * Join / split absolute path segments (kernel convention)
10239
+ ************************************************************/
10240
+ function seg_join(segments) {
10241
+ return segments.map(function(s) {
10242
+ return String(s);
10243
+ }).join("`");
10244
+ }
10245
+ function seg_split(path) {
10246
+ if (path === "" || path === null || path === void 0) return [];
10247
+ return path.split("`");
10248
+ }
10249
+ /************************************************************
10250
+ * Walk a JSON tree by absolute segments; undefined if the
10251
+ * path does not resolve.
10252
+ ************************************************************/
10253
+ function get_by_segments(root, segments) {
10254
+ let v = root;
10255
+ for (let seg of segments) {
10256
+ if (v === null || v === void 0) return;
10257
+ if (Array.isArray(v)) v = v[Number(seg)];
10258
+ else if (typeof v === "object") v = v[seg];
10259
+ else return;
10260
+ }
10261
+ return v;
10262
+ }
10263
+ /************************************************************
10264
+ * Set a value at absolute segments (mutates root).
10265
+ *
10266
+ * segments == [] means "replace the whole root" — the caller
10267
+ * must use the RETURNED value in that case (a primitive/array
10268
+ * root cannot be mutated in place).
10269
+ ************************************************************/
10270
+ function set_by_segments(root, segments, new_value) {
10271
+ if (segments.length === 0) return new_value;
10272
+ let parent = get_by_segments(root, segments.slice(0, -1));
10273
+ if (parent === null || parent === void 0 || typeof parent !== "object") return root;
10274
+ let last = segments[segments.length - 1];
10275
+ if (Array.isArray(parent)) parent[Number(last)] = new_value;
10276
+ else parent[last] = new_value;
10277
+ return root;
10278
+ }
10279
+ /************************************************************
10280
+ * Does `value` (or any loaded descendant / its own key)
10281
+ * contain the lower-cased search term?
10282
+ *
10283
+ * Collapsed (not-yet-loaded) subtrees can't be searched, so
10284
+ * they never match on content — only their key can match,
10285
+ * which the renderer checks separately.
10286
+ ************************************************************/
10287
+ function subtree_matches(value, term) {
10288
+ if (!term) return true;
10289
+ if (is_collapsed(value)) return false;
10290
+ let type = json_type(value);
10291
+ if (type === "object") {
10292
+ for (let [k, v] of Object.entries(value)) {
10293
+ if (String(k).toLowerCase().includes(term)) return true;
10294
+ if (subtree_matches(v, term)) return true;
10295
+ }
10296
+ return false;
10297
+ }
10298
+ if (type === "array") {
10299
+ for (let v of value) if (subtree_matches(v, term)) return true;
10300
+ return false;
10301
+ }
10302
+ return String(value).toLowerCase().includes(term);
10303
+ }
10304
+ /************************************************************
10305
+ * Fields whose numeric value is an epoch timestamp.
10306
+ * Mirrors the timestampTag list wired into C_YUI_FORM's
10307
+ * jsoneditor control, so the two viewers agree.
10308
+ ************************************************************/
10309
+ function is_time_field(field) {
10310
+ return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
10311
+ }
10312
+ /************************************************************
10313
+ * Format an epoch value as a local wall-clock string.
10314
+ * Accepts seconds or milliseconds; returns null when the
10315
+ * value is not a usable positive timestamp (0 == "unset").
10316
+ ************************************************************/
10317
+ function format_epoch(value) {
10318
+ if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
10319
+ let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
10320
+ let d = new Date(ms);
10321
+ if (isNaN(d.getTime())) return null;
10322
+ return d.toLocaleString();
10323
+ }
10324
+ /************************************************************
10325
+ * The text view's payload: the document as text, indented
10326
+ * four characters, cut at `max_chars`.
10327
+ *
10328
+ * Returns {text, capped}. `capped` is what the caller
10329
+ * announces under the dump — a cut that is not announced
10330
+ * reads as a document that ends there.
10331
+ ************************************************************/
10332
+ function json_text_dump(root, max_chars) {
10333
+ let text;
10334
+ try {
10335
+ text = JSON.stringify(root, null, 4);
10336
+ } catch (e) {
10337
+ return {
10338
+ text: "",
10339
+ capped: false,
10340
+ error: String(e)
10341
+ };
10342
+ }
10343
+ if (text === void 0) return {
10344
+ text: "",
10345
+ capped: false,
10346
+ error: "not representable as JSON"
10347
+ };
10348
+ if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
10349
+ text: text.slice(0, max_chars),
10350
+ capped: true
10351
+ };
10352
+ return {
10353
+ text,
10354
+ capped: false
10355
+ };
10356
+ }
10357
+ /************************************************************
10358
+ * Which view a freshly mounted viewer opens on.
10359
+ *
10360
+ * Three answers, in order of authority: what the HOST asked
10361
+ * for, what the READER chose last time, and `fallback`.
10362
+ *
10363
+ * The host wins because a viewer mounted to show a graph has
10364
+ * to show one. The memory comes next, because reopening a
10365
+ * document in the view you just left is the whole point of
10366
+ * remembering it. `fallback` is the floor.
10367
+ *
10368
+ * Anything not in `modes` is treated as unsaid, which covers
10369
+ * an absent value, an empty one, and a stored string from an
10370
+ * older release that named a view no longer offered.
10371
+ ************************************************************/
10372
+ function pick_view_mode(asked, remembered, modes, fallback) {
10373
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
10374
+ if (known(asked)) return asked;
10375
+ if (known(remembered)) return remembered;
10376
+ return fallback;
10377
+ }
10378
+ /************************************************************
10379
+ * The short label a container row carries beside its size.
10380
+ *
10381
+ * An array of dicts that carry an `id` is a list of RECORDS,
10382
+ * and a row that says only `2: {15}` makes you open all
10383
+ * fifteen fields of every one of them to find out WHICH
10384
+ * record it is. The id is the one field that answers that,
10385
+ * so the row says it without being opened.
10386
+ *
10387
+ * Only a SCALAR id, and only when it is really there: an `id`
10388
+ * that is itself a dict or a list is not a name, it is more
10389
+ * document, and printing `[object Object]` beside the size
10390
+ * would be worse than printing nothing.
10391
+ *
10392
+ * Cut at `max_chars`, because this rides on a row whose job
10393
+ * is to stay one line.
10394
+ ************************************************************/
10395
+ function container_label(value, max_chars) {
10396
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
10397
+ if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
10398
+ let id = value.id;
10399
+ let t = typeof id;
10400
+ if (t !== "string" && t !== "number" && t !== "boolean") return "";
10401
+ let text = String(id).trim();
10402
+ if (text === "") return "";
10403
+ let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
10404
+ if (cap && text.length > cap) return text.slice(0, cap) + "…";
10405
+ return text;
10406
+ }
10407
+ /************************************************************
10408
+ * A dict, in the JSON sense: not an array and not null.
10409
+ ************************************************************/
10410
+ function is_plain_object(v) {
10411
+ return v !== null && typeof v === "object" && !Array.isArray(v);
10412
+ }
10413
+ /************************************************************
10414
+ * Does this container hold ONLY other containers?
10415
+ *
10416
+ * Such a value is not a thing, it is a LIST of things -- and a
10417
+ * list is fully described by the row that names it in its
10418
+ * parent, plus the edges to what it holds. Drawing it as a
10419
+ * card of its own put a node in the middle of the graph with no
10420
+ * data in it, saying nothing the parent's row did not already
10421
+ * say, and pushed everything below it one level down.
10422
+ *
10423
+ * An EMPTY container inside counts as content: `{0}` is
10424
+ * something the card has to show, and there is no child card
10425
+ * to show it instead.
10426
+ ************************************************************/
10427
+ function is_pure_collection(value) {
10428
+ let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
10429
+ if (entries.length === 0) return false;
10430
+ for (let [, v] of entries) if (!has_branch(v)) return false;
10431
+ return true;
10432
+ }
10433
+ /************************************************************
10434
+ * A value that gets a card, or children, of its own.
10435
+ ************************************************************/
10436
+ function has_branch(value) {
10437
+ return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
10438
+ }
10439
+ /************************************************************
10440
+ * How many branches a container has, which is what the fold
10441
+ * chip counts.
10442
+ ************************************************************/
10443
+ function count_branches(value) {
10444
+ let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
10445
+ let n = 0;
10446
+ for (let v of entries) if (has_branch(v)) n++;
10447
+ return n;
10448
+ }
10162
10449
  //#endregion
10163
10450
  //#region src/c_yui_json_graph.js
10164
10451
  /***********************************************************************
@@ -10667,9 +10954,19 @@ function build_graph$3(gobj) {
10667
10954
  }
10668
10955
  }
10669
10956
  /************************************************************
10957
+ * The port a container key opens on its card.
10958
+ *
10959
+ * Prefixed so it can never collide with anything G6 puts in
10960
+ * the same namespace, and stringified because an array index
10961
+ * is a number and a port key is a string.
10962
+ ************************************************************/
10963
+ function port_key(key) {
10964
+ return "p_" + String(key);
10965
+ }
10966
+ /************************************************************
10670
10967
  * Build cell value HTML (matching old mx_json_viewer colors)
10671
10968
  ************************************************************/
10672
- function build_cell_html(key, value, type, dark, matched) {
10969
+ function build_cell_html(key, value, type, dark, matched, fold) {
10673
10970
  let on_light = matched ? false : dark;
10674
10971
  let color = type_color(type, on_light);
10675
10972
  let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
@@ -10696,6 +10993,7 @@ function build_cell_html(key, value, type, dark, matched) {
10696
10993
  default: display_value = String(value);
10697
10994
  }
10698
10995
  let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
10996
+ if (fold) row += `<span style="margin-left:8px;">` + fold_toggle_html(fold.path, true, fold.folded, fold.branches, fold.colors) + `</span>`;
10699
10997
  if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
10700
10998
  return row;
10701
10999
  }
@@ -10808,7 +11106,7 @@ function get_group_label(path) {
10808
11106
  /************************************************************
10809
11107
  * Recursively build nodes and edges from JSON
10810
11108
  ************************************************************/
10811
- function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
11109
+ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port) {
10812
11110
  let group_id = gen_node_id$1(gobj);
10813
11111
  let is_dict = is_object(kw);
10814
11112
  let is_list = is_array(kw);
@@ -10819,6 +11117,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10819
11117
  let lines = [];
10820
11118
  let pending_complex = [];
10821
11119
  let card_matched = false;
11120
+ let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
10822
11121
  let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
10823
11122
  for (let [key, value] of entries) {
10824
11123
  let type = get_json_type(value);
@@ -10827,20 +11126,33 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10827
11126
  card_matched = true;
10828
11127
  priv.match_count++;
10829
11128
  }
10830
- if (is_object(value) && json_object_size(value) > 0 || is_array(value) && value.length > 0) {
11129
+ let fold = null;
11130
+ if (has_branch(value)) {
10831
11131
  pending_complex.push({
10832
11132
  key,
10833
- value
11133
+ value,
11134
+ port: port_key(key)
10834
11135
  });
10835
- continue;
11136
+ if (is_pure_collection(value)) {
11137
+ let child_path = add_segment(path, key);
11138
+ fold = {
11139
+ path: child_path,
11140
+ folded: priv.collapsed.has(child_path),
11141
+ branches: count_branches(value),
11142
+ colors
11143
+ };
11144
+ }
10836
11145
  }
10837
- lines.push(build_cell_html(key, value, type, dark, matched));
11146
+ lines.push(build_cell_html(key, value, type, dark, matched, fold));
11147
+ }
11148
+ if (parent_id && is_pure_collection(kw)) {
11149
+ if (priv.collapsed.has(path)) return parent_id;
11150
+ for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id, parent_port);
11151
+ return parent_id;
10838
11152
  }
10839
11153
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
10840
- if (pending_complex.length > 0) label += is_dict ? ` {${pending_complex.length}}` : ` [${pending_complex.length}]`;
10841
11154
  let foldable = pending_complex.length > 0;
10842
11155
  let folded = foldable && priv.collapsed.has(path);
10843
- let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
10844
11156
  let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
10845
11157
  let border_width = card_matched ? 2 : 1;
10846
11158
  let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
@@ -10866,6 +11178,17 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10866
11178
  ">${FOLD_SPACER}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span>${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}</div>
10867
11179
  ${content_html}
10868
11180
  </div>`;
11181
+ let ports = [];
11182
+ for (let i = 0; i < pending_complex.length; i++) {
11183
+ let pending = pending_complex[i];
11184
+ let child_colors = json_card_style(is_object(pending.value) ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
11185
+ ports.push({
11186
+ key: pending.port,
11187
+ placement: [getPointPosition(pending_complex.length, i), 1],
11188
+ fill: child_colors.border,
11189
+ stroke: colors.border
11190
+ });
11191
+ }
10869
11192
  let node = {
10870
11193
  id: group_id,
10871
11194
  type: "html",
@@ -10877,20 +11200,28 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10877
11200
  innerHTML: node_html,
10878
11201
  size: [min_width, 24 + lines.length * 22],
10879
11202
  dx: -(min_width / 2),
10880
- dy: -((24 + lines.length * 22) / 2)
11203
+ dy: -((24 + lines.length * 22) / 2),
11204
+ port: ports.length > 0,
11205
+ ports,
11206
+ portR: 4,
11207
+ portLineWidth: 1
10881
11208
  }
10882
11209
  };
10883
11210
  nodes.push(node);
10884
- if (parent_id) edges.push({
10885
- source: parent_id,
10886
- target: group_id,
10887
- style: {
10888
- stroke: colors.border,
10889
- lineWidth: 1
10890
- }
10891
- });
11211
+ if (parent_id) {
11212
+ let edge = {
11213
+ source: parent_id,
11214
+ target: group_id,
11215
+ style: {
11216
+ stroke: colors.border,
11217
+ lineWidth: 1
11218
+ }
11219
+ };
11220
+ if (parent_port) edge.style.sourcePort = parent_port;
11221
+ edges.push(edge);
11222
+ }
10892
11223
  if (folded) return group_id;
10893
- for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id);
11224
+ for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id, pending.port);
10894
11225
  return group_id;
10895
11226
  }
10896
11227
  /************************************************************
@@ -10934,22 +11265,25 @@ function load_json(gobj) {
10934
11265
  }
10935
11266
  /************************************************************
10936
11267
  * Every path that HAS a branch to fold, in the same recursion
10937
- * build_json_nodes uses — so "collapse all" can never name a
10938
- * card that does not exist. The root is excluded: folding it
11268
+ * build_json_nodes uses — so "collapse all" can never name
11269
+ * something with no handle. The handle is a card's header chip
11270
+ * for a container that gets a card, and the chip on its ROW in
11271
+ * the parent for a pure collection, which gets none; both are
11272
+ * keyed by this same path. The root is excluded: folding it
10939
11273
  * would leave an empty canvas.
10940
11274
  ************************************************************/
10941
11275
  function collect_foldable_paths(value, path, out, is_root) {
10942
11276
  let is_dict = is_object(value);
10943
11277
  let is_list = is_array(value);
10944
11278
  if (!is_dict && !is_list) return false;
10945
- let has_branch = false;
11279
+ let found_branch = false;
10946
11280
  let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
10947
- for (let [key, child] of entries) if (is_object(child) && json_object_size(child) > 0 || is_array(child) && child.length > 0) {
10948
- has_branch = true;
11281
+ for (let [key, child] of entries) if (has_branch(child)) {
11282
+ found_branch = true;
10949
11283
  collect_foldable_paths(child, add_segment(path, key), out, false);
10950
11284
  }
10951
- if (has_branch && !is_root) out.push(path);
10952
- return has_branch;
11285
+ if (found_branch && !is_root) out.push(path);
11286
+ return found_branch;
10953
11287
  }
10954
11288
  /************************************************************
10955
11289
  * Show the find count, or hide the chip when there is no
@@ -11508,226 +11842,6 @@ function register_c_yui_json_graph() {
11508
11842
  if (gclass_find_by_name(GCLASS_NAME$11, false)) return 0;
11509
11843
  return create_gclass$11(GCLASS_NAME$11);
11510
11844
  }
11511
- /************************************************************
11512
- * JSON type discriminator (viewer vocabulary)
11513
- ************************************************************/
11514
- function json_type(value) {
11515
- if (value === null) return "null";
11516
- if (Array.isArray(value)) return "array";
11517
- switch (typeof value) {
11518
- case "string": return "string";
11519
- case "number": return "number";
11520
- case "boolean": return "boolean";
11521
- case "object": return "object";
11522
- default: return "unknown";
11523
- }
11524
- }
11525
- /************************************************************
11526
- * Detect a kw_collapse() sentinel.
11527
- *
11528
- * The kernel replaces an over-limit dict with:
11529
- * { "__collapsed__": { "path": ..., "size": N } }
11530
- * and an over-limit array with:
11531
- * [ { "__collapsed__": { "path": ..., "size": N } } ]
11532
- *
11533
- * Returns { size, path, is_array } or null.
11534
- ************************************************************/
11535
- function is_collapsed(value) {
11536
- if (value && typeof value === "object" && !Array.isArray(value)) {
11537
- let keys = Object.keys(value);
11538
- if (keys.length === 1 && keys[0] === "__collapsed__") {
11539
- let c = value.__collapsed__ || {};
11540
- return {
11541
- size: c.size,
11542
- path: c.path,
11543
- is_array: false
11544
- };
11545
- }
11546
- return null;
11547
- }
11548
- if (Array.isArray(value) && value.length === 1) {
11549
- let e = value[0];
11550
- if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
11551
- let c = e.__collapsed__ || {};
11552
- return {
11553
- size: c.size,
11554
- path: c.path,
11555
- is_array: true
11556
- };
11557
- }
11558
- }
11559
- return null;
11560
- }
11561
- /************************************************************
11562
- * Join / split absolute path segments (kernel convention)
11563
- ************************************************************/
11564
- function seg_join(segments) {
11565
- return segments.map(function(s) {
11566
- return String(s);
11567
- }).join("`");
11568
- }
11569
- function seg_split(path) {
11570
- if (path === "" || path === null || path === void 0) return [];
11571
- return path.split("`");
11572
- }
11573
- /************************************************************
11574
- * Walk a JSON tree by absolute segments; undefined if the
11575
- * path does not resolve.
11576
- ************************************************************/
11577
- function get_by_segments(root, segments) {
11578
- let v = root;
11579
- for (let seg of segments) {
11580
- if (v === null || v === void 0) return;
11581
- if (Array.isArray(v)) v = v[Number(seg)];
11582
- else if (typeof v === "object") v = v[seg];
11583
- else return;
11584
- }
11585
- return v;
11586
- }
11587
- /************************************************************
11588
- * Set a value at absolute segments (mutates root).
11589
- *
11590
- * segments == [] means "replace the whole root" — the caller
11591
- * must use the RETURNED value in that case (a primitive/array
11592
- * root cannot be mutated in place).
11593
- ************************************************************/
11594
- function set_by_segments(root, segments, new_value) {
11595
- if (segments.length === 0) return new_value;
11596
- let parent = get_by_segments(root, segments.slice(0, -1));
11597
- if (parent === null || parent === void 0 || typeof parent !== "object") return root;
11598
- let last = segments[segments.length - 1];
11599
- if (Array.isArray(parent)) parent[Number(last)] = new_value;
11600
- else parent[last] = new_value;
11601
- return root;
11602
- }
11603
- /************************************************************
11604
- * Does `value` (or any loaded descendant / its own key)
11605
- * contain the lower-cased search term?
11606
- *
11607
- * Collapsed (not-yet-loaded) subtrees can't be searched, so
11608
- * they never match on content — only their key can match,
11609
- * which the renderer checks separately.
11610
- ************************************************************/
11611
- function subtree_matches(value, term) {
11612
- if (!term) return true;
11613
- if (is_collapsed(value)) return false;
11614
- let type = json_type(value);
11615
- if (type === "object") {
11616
- for (let [k, v] of Object.entries(value)) {
11617
- if (String(k).toLowerCase().includes(term)) return true;
11618
- if (subtree_matches(v, term)) return true;
11619
- }
11620
- return false;
11621
- }
11622
- if (type === "array") {
11623
- for (let v of value) if (subtree_matches(v, term)) return true;
11624
- return false;
11625
- }
11626
- return String(value).toLowerCase().includes(term);
11627
- }
11628
- /************************************************************
11629
- * Fields whose numeric value is an epoch timestamp.
11630
- * Mirrors the timestampTag list wired into C_YUI_FORM's
11631
- * jsoneditor control, so the two viewers agree.
11632
- ************************************************************/
11633
- function is_time_field(field) {
11634
- return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
11635
- }
11636
- /************************************************************
11637
- * Format an epoch value as a local wall-clock string.
11638
- * Accepts seconds or milliseconds; returns null when the
11639
- * value is not a usable positive timestamp (0 == "unset").
11640
- ************************************************************/
11641
- function format_epoch(value) {
11642
- if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
11643
- let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
11644
- let d = new Date(ms);
11645
- if (isNaN(d.getTime())) return null;
11646
- return d.toLocaleString();
11647
- }
11648
- /************************************************************
11649
- * The text view's payload: the document as text, indented
11650
- * four characters, cut at `max_chars`.
11651
- *
11652
- * Returns {text, capped}. `capped` is what the caller
11653
- * announces under the dump — a cut that is not announced
11654
- * reads as a document that ends there.
11655
- ************************************************************/
11656
- function json_text_dump(root, max_chars) {
11657
- let text;
11658
- try {
11659
- text = JSON.stringify(root, null, 4);
11660
- } catch (e) {
11661
- return {
11662
- text: "",
11663
- capped: false,
11664
- error: String(e)
11665
- };
11666
- }
11667
- if (text === void 0) return {
11668
- text: "",
11669
- capped: false,
11670
- error: "not representable as JSON"
11671
- };
11672
- if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
11673
- text: text.slice(0, max_chars),
11674
- capped: true
11675
- };
11676
- return {
11677
- text,
11678
- capped: false
11679
- };
11680
- }
11681
- /************************************************************
11682
- * Which view a freshly mounted viewer opens on.
11683
- *
11684
- * Three answers, in order of authority: what the HOST asked
11685
- * for, what the READER chose last time, and `fallback`.
11686
- *
11687
- * The host wins because a viewer mounted to show a graph has
11688
- * to show one. The memory comes next, because reopening a
11689
- * document in the view you just left is the whole point of
11690
- * remembering it. `fallback` is the floor.
11691
- *
11692
- * Anything not in `modes` is treated as unsaid, which covers
11693
- * an absent value, an empty one, and a stored string from an
11694
- * older release that named a view no longer offered.
11695
- ************************************************************/
11696
- function pick_view_mode(asked, remembered, modes, fallback) {
11697
- let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
11698
- if (known(asked)) return asked;
11699
- if (known(remembered)) return remembered;
11700
- return fallback;
11701
- }
11702
- /************************************************************
11703
- * The short label a container row carries beside its size.
11704
- *
11705
- * An array of dicts that carry an `id` is a list of RECORDS,
11706
- * and a row that says only `2: {15}` makes you open all
11707
- * fifteen fields of every one of them to find out WHICH
11708
- * record it is. The id is the one field that answers that,
11709
- * so the row says it without being opened.
11710
- *
11711
- * Only a SCALAR id, and only when it is really there: an `id`
11712
- * that is itself a dict or a list is not a name, it is more
11713
- * document, and printing `[object Object]` beside the size
11714
- * would be worse than printing nothing.
11715
- *
11716
- * Cut at `max_chars`, because this rides on a row whose job
11717
- * is to stay one line.
11718
- ************************************************************/
11719
- function container_label(value, max_chars) {
11720
- if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
11721
- if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
11722
- let id = value.id;
11723
- let t = typeof id;
11724
- if (t !== "string" && t !== "number" && t !== "boolean") return "";
11725
- let text = String(id).trim();
11726
- if (text === "") return "";
11727
- let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
11728
- if (cap && text.length > cap) return text.slice(0, cap) + "…";
11729
- return text;
11730
- }
11731
11845
  //#endregion
11732
11846
  //#region src/c_yui_json.js
11733
11847
  /***********************************************************************
@@ -48960,43 +49074,6 @@ function configure_behaviour(gobj) {
48960
49074
  graph_write_behaviors(gobj, behaviors);
48961
49075
  }
48962
49076
  /************************************************************
48963
- * Returns the proportional position (between 0 and 1) of a specific point,
48964
- * centered and spaced with margins.
48965
- *
48966
- * index - Index of the point (0 to count-1)
48967
- * count - Total number of points
48968
- * margin - Total margin space (default 0.2 means 10% on each end)
48969
- ************************************************************/
48970
- function getPointPosition(count, index, margin = .2) {
48971
- if (count <= 0 || index < 0 || index >= count) {
48972
- log_error("Invalid count or index");
48973
- return .5;
48974
- }
48975
- const start = margin / 2;
48976
- const step = (1 - margin / 2 - start) / count;
48977
- return start + index * step + step / 2;
48978
- }
48979
- /************************************************************
48980
- * Count hooks and fkeys in topic desc, classify node type
48981
- ************************************************************/
48982
- function calculate_hooks_fkeys_counter(desc) {
48983
- let cols = desc.cols;
48984
- desc.hooks_counter = 0;
48985
- desc.fkeys_counter = 0;
48986
- for (let i = 0; i < cols.length; i++) {
48987
- let col = cols[i];
48988
- switch (treedb_get_field_desc(col).type) {
48989
- case "hook":
48990
- desc.hooks_counter++;
48991
- break;
48992
- case "fkey": desc.fkeys_counter++;
48993
- }
48994
- }
48995
- if (desc.hooks_counter === 0) desc.node_treedb_type = "child";
48996
- else if (desc.fkeys_counter === 0) desc.node_treedb_type = "extended";
48997
- else desc.node_treedb_type = "hierarchical";
48998
- }
48999
- /************************************************************
49000
49077
  * Build ports for a topic desc
49001
49078
  ************************************************************/
49002
49079
  function build_ports(gobj, desc) {