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