@yuneta/gobj-ui 7.23.19 → 7.23.20
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/dist/gobj-ui.cjs.js +292 -233
- package/dist/gobj-ui.es.js +292 -233
- package/package.json +1 -1
- package/src/c_yui_json_graph.js +84 -29
- package/src/json_view_helpers.js +67 -0
- package/src/json_view_helpers.test.js +67 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -10172,6 +10172,268 @@ function ensure_drag_canvas_patch() {
|
|
|
10172
10172
|
(0, _antv_g6.register)(_antv_g6.ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
|
|
10173
10173
|
__drag_canvas_patched__ = true;
|
|
10174
10174
|
}
|
|
10175
|
+
/************************************************************
|
|
10176
|
+
* JSON type discriminator (viewer vocabulary)
|
|
10177
|
+
************************************************************/
|
|
10178
|
+
function json_type(value) {
|
|
10179
|
+
if (value === null) return "null";
|
|
10180
|
+
if (Array.isArray(value)) return "array";
|
|
10181
|
+
switch (typeof value) {
|
|
10182
|
+
case "string": return "string";
|
|
10183
|
+
case "number": return "number";
|
|
10184
|
+
case "boolean": return "boolean";
|
|
10185
|
+
case "object": return "object";
|
|
10186
|
+
default: return "unknown";
|
|
10187
|
+
}
|
|
10188
|
+
}
|
|
10189
|
+
/************************************************************
|
|
10190
|
+
* Detect a kw_collapse() sentinel.
|
|
10191
|
+
*
|
|
10192
|
+
* The kernel replaces an over-limit dict with:
|
|
10193
|
+
* { "__collapsed__": { "path": ..., "size": N } }
|
|
10194
|
+
* and an over-limit array with:
|
|
10195
|
+
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
10196
|
+
*
|
|
10197
|
+
* Returns { size, path, is_array } or null.
|
|
10198
|
+
************************************************************/
|
|
10199
|
+
function is_collapsed(value) {
|
|
10200
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10201
|
+
let keys = Object.keys(value);
|
|
10202
|
+
if (keys.length === 1 && keys[0] === "__collapsed__") {
|
|
10203
|
+
let c = value.__collapsed__ || {};
|
|
10204
|
+
return {
|
|
10205
|
+
size: c.size,
|
|
10206
|
+
path: c.path,
|
|
10207
|
+
is_array: false
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
return null;
|
|
10211
|
+
}
|
|
10212
|
+
if (Array.isArray(value) && value.length === 1) {
|
|
10213
|
+
let e = value[0];
|
|
10214
|
+
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
10215
|
+
let c = e.__collapsed__ || {};
|
|
10216
|
+
return {
|
|
10217
|
+
size: c.size,
|
|
10218
|
+
path: c.path,
|
|
10219
|
+
is_array: true
|
|
10220
|
+
};
|
|
10221
|
+
}
|
|
10222
|
+
}
|
|
10223
|
+
return null;
|
|
10224
|
+
}
|
|
10225
|
+
/************************************************************
|
|
10226
|
+
* Join / split absolute path segments (kernel convention)
|
|
10227
|
+
************************************************************/
|
|
10228
|
+
function seg_join(segments) {
|
|
10229
|
+
return segments.map(function(s) {
|
|
10230
|
+
return String(s);
|
|
10231
|
+
}).join("`");
|
|
10232
|
+
}
|
|
10233
|
+
function seg_split(path) {
|
|
10234
|
+
if (path === "" || path === null || path === void 0) return [];
|
|
10235
|
+
return path.split("`");
|
|
10236
|
+
}
|
|
10237
|
+
/************************************************************
|
|
10238
|
+
* Walk a JSON tree by absolute segments; undefined if the
|
|
10239
|
+
* path does not resolve.
|
|
10240
|
+
************************************************************/
|
|
10241
|
+
function get_by_segments(root, segments) {
|
|
10242
|
+
let v = root;
|
|
10243
|
+
for (let seg of segments) {
|
|
10244
|
+
if (v === null || v === void 0) return;
|
|
10245
|
+
if (Array.isArray(v)) v = v[Number(seg)];
|
|
10246
|
+
else if (typeof v === "object") v = v[seg];
|
|
10247
|
+
else return;
|
|
10248
|
+
}
|
|
10249
|
+
return v;
|
|
10250
|
+
}
|
|
10251
|
+
/************************************************************
|
|
10252
|
+
* Set a value at absolute segments (mutates root).
|
|
10253
|
+
*
|
|
10254
|
+
* segments == [] means "replace the whole root" — the caller
|
|
10255
|
+
* must use the RETURNED value in that case (a primitive/array
|
|
10256
|
+
* root cannot be mutated in place).
|
|
10257
|
+
************************************************************/
|
|
10258
|
+
function set_by_segments(root, segments, new_value) {
|
|
10259
|
+
if (segments.length === 0) return new_value;
|
|
10260
|
+
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
10261
|
+
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
10262
|
+
let last = segments[segments.length - 1];
|
|
10263
|
+
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
10264
|
+
else parent[last] = new_value;
|
|
10265
|
+
return root;
|
|
10266
|
+
}
|
|
10267
|
+
/************************************************************
|
|
10268
|
+
* Does `value` (or any loaded descendant / its own key)
|
|
10269
|
+
* contain the lower-cased search term?
|
|
10270
|
+
*
|
|
10271
|
+
* Collapsed (not-yet-loaded) subtrees can't be searched, so
|
|
10272
|
+
* they never match on content — only their key can match,
|
|
10273
|
+
* which the renderer checks separately.
|
|
10274
|
+
************************************************************/
|
|
10275
|
+
function subtree_matches(value, term) {
|
|
10276
|
+
if (!term) return true;
|
|
10277
|
+
if (is_collapsed(value)) return false;
|
|
10278
|
+
let type = json_type(value);
|
|
10279
|
+
if (type === "object") {
|
|
10280
|
+
for (let [k, v] of Object.entries(value)) {
|
|
10281
|
+
if (String(k).toLowerCase().includes(term)) return true;
|
|
10282
|
+
if (subtree_matches(v, term)) return true;
|
|
10283
|
+
}
|
|
10284
|
+
return false;
|
|
10285
|
+
}
|
|
10286
|
+
if (type === "array") {
|
|
10287
|
+
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
10288
|
+
return false;
|
|
10289
|
+
}
|
|
10290
|
+
return String(value).toLowerCase().includes(term);
|
|
10291
|
+
}
|
|
10292
|
+
/************************************************************
|
|
10293
|
+
* Fields whose numeric value is an epoch timestamp.
|
|
10294
|
+
* Mirrors the timestampTag list wired into C_YUI_FORM's
|
|
10295
|
+
* jsoneditor control, so the two viewers agree.
|
|
10296
|
+
************************************************************/
|
|
10297
|
+
function is_time_field(field) {
|
|
10298
|
+
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";
|
|
10299
|
+
}
|
|
10300
|
+
/************************************************************
|
|
10301
|
+
* Format an epoch value as a local wall-clock string.
|
|
10302
|
+
* Accepts seconds or milliseconds; returns null when the
|
|
10303
|
+
* value is not a usable positive timestamp (0 == "unset").
|
|
10304
|
+
************************************************************/
|
|
10305
|
+
function format_epoch(value) {
|
|
10306
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
10307
|
+
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
10308
|
+
let d = new Date(ms);
|
|
10309
|
+
if (isNaN(d.getTime())) return null;
|
|
10310
|
+
return d.toLocaleString();
|
|
10311
|
+
}
|
|
10312
|
+
/************************************************************
|
|
10313
|
+
* The text view's payload: the document as text, indented
|
|
10314
|
+
* four characters, cut at `max_chars`.
|
|
10315
|
+
*
|
|
10316
|
+
* Returns {text, capped}. `capped` is what the caller
|
|
10317
|
+
* announces under the dump — a cut that is not announced
|
|
10318
|
+
* reads as a document that ends there.
|
|
10319
|
+
************************************************************/
|
|
10320
|
+
function json_text_dump(root, max_chars) {
|
|
10321
|
+
let text;
|
|
10322
|
+
try {
|
|
10323
|
+
text = JSON.stringify(root, null, 4);
|
|
10324
|
+
} catch (e) {
|
|
10325
|
+
return {
|
|
10326
|
+
text: "",
|
|
10327
|
+
capped: false,
|
|
10328
|
+
error: String(e)
|
|
10329
|
+
};
|
|
10330
|
+
}
|
|
10331
|
+
if (text === void 0) return {
|
|
10332
|
+
text: "",
|
|
10333
|
+
capped: false,
|
|
10334
|
+
error: "not representable as JSON"
|
|
10335
|
+
};
|
|
10336
|
+
if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
|
|
10337
|
+
text: text.slice(0, max_chars),
|
|
10338
|
+
capped: true
|
|
10339
|
+
};
|
|
10340
|
+
return {
|
|
10341
|
+
text,
|
|
10342
|
+
capped: false
|
|
10343
|
+
};
|
|
10344
|
+
}
|
|
10345
|
+
/************************************************************
|
|
10346
|
+
* Which view a freshly mounted viewer opens on.
|
|
10347
|
+
*
|
|
10348
|
+
* Three answers, in order of authority: what the HOST asked
|
|
10349
|
+
* for, what the READER chose last time, and `fallback`.
|
|
10350
|
+
*
|
|
10351
|
+
* The host wins because a viewer mounted to show a graph has
|
|
10352
|
+
* to show one. The memory comes next, because reopening a
|
|
10353
|
+
* document in the view you just left is the whole point of
|
|
10354
|
+
* remembering it. `fallback` is the floor.
|
|
10355
|
+
*
|
|
10356
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
10357
|
+
* an absent value, an empty one, and a stored string from an
|
|
10358
|
+
* older release that named a view no longer offered.
|
|
10359
|
+
************************************************************/
|
|
10360
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
10361
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
10362
|
+
if (known(asked)) return asked;
|
|
10363
|
+
if (known(remembered)) return remembered;
|
|
10364
|
+
return fallback;
|
|
10365
|
+
}
|
|
10366
|
+
/************************************************************
|
|
10367
|
+
* The short label a container row carries beside its size.
|
|
10368
|
+
*
|
|
10369
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
10370
|
+
* and a row that says only `2: {15}` makes you open all
|
|
10371
|
+
* fifteen fields of every one of them to find out WHICH
|
|
10372
|
+
* record it is. The id is the one field that answers that,
|
|
10373
|
+
* so the row says it without being opened.
|
|
10374
|
+
*
|
|
10375
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
10376
|
+
* that is itself a dict or a list is not a name, it is more
|
|
10377
|
+
* document, and printing `[object Object]` beside the size
|
|
10378
|
+
* would be worse than printing nothing.
|
|
10379
|
+
*
|
|
10380
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
10381
|
+
* is to stay one line.
|
|
10382
|
+
************************************************************/
|
|
10383
|
+
function container_label(value, max_chars) {
|
|
10384
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
10385
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
10386
|
+
let id = value.id;
|
|
10387
|
+
let t = typeof id;
|
|
10388
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
10389
|
+
let text = String(id).trim();
|
|
10390
|
+
if (text === "") return "";
|
|
10391
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
10392
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
10393
|
+
return text;
|
|
10394
|
+
}
|
|
10395
|
+
/************************************************************
|
|
10396
|
+
* A dict, in the JSON sense: not an array and not null.
|
|
10397
|
+
************************************************************/
|
|
10398
|
+
function is_plain_object(v) {
|
|
10399
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10400
|
+
}
|
|
10401
|
+
/************************************************************
|
|
10402
|
+
* Does this container hold ONLY other containers?
|
|
10403
|
+
*
|
|
10404
|
+
* Such a value is not a thing, it is a LIST of things -- and a
|
|
10405
|
+
* list is fully described by the row that names it in its
|
|
10406
|
+
* parent, plus the edges to what it holds. Drawing it as a
|
|
10407
|
+
* card of its own put a node in the middle of the graph with no
|
|
10408
|
+
* data in it, saying nothing the parent's row did not already
|
|
10409
|
+
* say, and pushed everything below it one level down.
|
|
10410
|
+
*
|
|
10411
|
+
* An EMPTY container inside counts as content: `{0}` is
|
|
10412
|
+
* something the card has to show, and there is no child card
|
|
10413
|
+
* to show it instead.
|
|
10414
|
+
************************************************************/
|
|
10415
|
+
function is_pure_collection(value) {
|
|
10416
|
+
let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
|
|
10417
|
+
if (entries.length === 0) return false;
|
|
10418
|
+
for (let [, v] of entries) if (!has_branch(v)) return false;
|
|
10419
|
+
return true;
|
|
10420
|
+
}
|
|
10421
|
+
/************************************************************
|
|
10422
|
+
* A value that gets a card, or children, of its own.
|
|
10423
|
+
************************************************************/
|
|
10424
|
+
function has_branch(value) {
|
|
10425
|
+
return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
|
|
10426
|
+
}
|
|
10427
|
+
/************************************************************
|
|
10428
|
+
* How many branches a container has, which is what the fold
|
|
10429
|
+
* chip counts.
|
|
10430
|
+
************************************************************/
|
|
10431
|
+
function count_branches(value) {
|
|
10432
|
+
let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
|
|
10433
|
+
let n = 0;
|
|
10434
|
+
for (let v of entries) if (has_branch(v)) n++;
|
|
10435
|
+
return n;
|
|
10436
|
+
}
|
|
10175
10437
|
//#endregion
|
|
10176
10438
|
//#region src/c_yui_json_graph.js
|
|
10177
10439
|
/***********************************************************************
|
|
@@ -10682,7 +10944,7 @@ function build_graph$3(gobj) {
|
|
|
10682
10944
|
/************************************************************
|
|
10683
10945
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10684
10946
|
************************************************************/
|
|
10685
|
-
function build_cell_html(key, value, type, dark, matched) {
|
|
10947
|
+
function build_cell_html(key, value, type, dark, matched, fold) {
|
|
10686
10948
|
let on_light = matched ? false : dark;
|
|
10687
10949
|
let color = type_color(type, on_light);
|
|
10688
10950
|
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
@@ -10709,6 +10971,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10709
10971
|
default: display_value = String(value);
|
|
10710
10972
|
}
|
|
10711
10973
|
let row = `<span style="color:${key_color}">• ${(0, _yuneta_gobj_js.escapeHtml)(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
10974
|
+
if (fold) row += `<span style="margin-left:8px;">` + fold_toggle_html(fold.path, true, fold.folded, fold.branches, fold.colors) + `</span>`;
|
|
10712
10975
|
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10713
10976
|
return row;
|
|
10714
10977
|
}
|
|
@@ -10832,6 +11095,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
10832
11095
|
let lines = [];
|
|
10833
11096
|
let pending_complex = [];
|
|
10834
11097
|
let card_matched = false;
|
|
11098
|
+
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
10835
11099
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
10836
11100
|
for (let [key, value] of entries) {
|
|
10837
11101
|
let type = get_json_type(value);
|
|
@@ -10840,20 +11104,32 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
10840
11104
|
card_matched = true;
|
|
10841
11105
|
priv.match_count++;
|
|
10842
11106
|
}
|
|
10843
|
-
|
|
11107
|
+
let fold = null;
|
|
11108
|
+
if (has_branch(value)) {
|
|
10844
11109
|
pending_complex.push({
|
|
10845
11110
|
key,
|
|
10846
11111
|
value
|
|
10847
11112
|
});
|
|
10848
|
-
|
|
11113
|
+
if (is_pure_collection(value)) {
|
|
11114
|
+
let child_path = add_segment(path, key);
|
|
11115
|
+
fold = {
|
|
11116
|
+
path: child_path,
|
|
11117
|
+
folded: priv.collapsed.has(child_path),
|
|
11118
|
+
branches: count_branches(value),
|
|
11119
|
+
colors
|
|
11120
|
+
};
|
|
11121
|
+
}
|
|
10849
11122
|
}
|
|
10850
|
-
lines.push(build_cell_html(key, value, type, dark, matched));
|
|
11123
|
+
lines.push(build_cell_html(key, value, type, dark, matched, fold));
|
|
11124
|
+
}
|
|
11125
|
+
if (parent_id && is_pure_collection(kw)) {
|
|
11126
|
+
if (priv.collapsed.has(path)) return parent_id;
|
|
11127
|
+
for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id);
|
|
11128
|
+
return parent_id;
|
|
10851
11129
|
}
|
|
10852
11130
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
10853
|
-
if (pending_complex.length > 0) label += is_dict ? ` {${pending_complex.length}}` : ` [${pending_complex.length}]`;
|
|
10854
11131
|
let foldable = pending_complex.length > 0;
|
|
10855
11132
|
let folded = foldable && priv.collapsed.has(path);
|
|
10856
|
-
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
10857
11133
|
let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
|
|
10858
11134
|
let border_width = card_matched ? 2 : 1;
|
|
10859
11135
|
let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
|
|
@@ -10947,22 +11223,25 @@ function load_json(gobj) {
|
|
|
10947
11223
|
}
|
|
10948
11224
|
/************************************************************
|
|
10949
11225
|
* Every path that HAS a branch to fold, in the same recursion
|
|
10950
|
-
* build_json_nodes uses — so "collapse all" can never name
|
|
10951
|
-
*
|
|
11226
|
+
* build_json_nodes uses — so "collapse all" can never name
|
|
11227
|
+
* something with no handle. The handle is a card's header chip
|
|
11228
|
+
* for a container that gets a card, and the chip on its ROW in
|
|
11229
|
+
* the parent for a pure collection, which gets none; both are
|
|
11230
|
+
* keyed by this same path. The root is excluded: folding it
|
|
10952
11231
|
* would leave an empty canvas.
|
|
10953
11232
|
************************************************************/
|
|
10954
11233
|
function collect_foldable_paths(value, path, out, is_root) {
|
|
10955
11234
|
let is_dict = (0, _yuneta_gobj_js.is_object)(value);
|
|
10956
11235
|
let is_list = (0, _yuneta_gobj_js.is_array)(value);
|
|
10957
11236
|
if (!is_dict && !is_list) return false;
|
|
10958
|
-
let
|
|
11237
|
+
let found_branch = false;
|
|
10959
11238
|
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
10960
|
-
for (let [key, child] of entries) if ((
|
|
10961
|
-
|
|
11239
|
+
for (let [key, child] of entries) if (has_branch(child)) {
|
|
11240
|
+
found_branch = true;
|
|
10962
11241
|
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
10963
11242
|
}
|
|
10964
|
-
if (
|
|
10965
|
-
return
|
|
11243
|
+
if (found_branch && !is_root) out.push(path);
|
|
11244
|
+
return found_branch;
|
|
10966
11245
|
}
|
|
10967
11246
|
/************************************************************
|
|
10968
11247
|
* Show the find count, or hide the chip when there is no
|
|
@@ -11521,226 +11800,6 @@ function register_c_yui_json_graph() {
|
|
|
11521
11800
|
if ((0, _yuneta_gobj_js.gclass_find_by_name)(GCLASS_NAME$11, false)) return 0;
|
|
11522
11801
|
return create_gclass$11(GCLASS_NAME$11);
|
|
11523
11802
|
}
|
|
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
11803
|
//#endregion
|
|
11745
11804
|
//#region src/c_yui_json.js
|
|
11746
11805
|
/***********************************************************************
|