@yuneta/gobj-ui 7.19.4 → 7.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/c_yui_json.js CHANGED
@@ -24,9 +24,19 @@
24
24
  * Only expanded containers are materialised in the DOM, so the tree
25
25
  * stays bounded no matter how large the source document is.
26
26
  *
27
+ * TWO VIEWS over the same working document, chosen by the `view_mode`
28
+ * attr ("tree" | "text") and switched from the toolbar: the lazy tree
29
+ * above, and the raw `JSON.stringify(…, 4)` dump of what is currently
30
+ * loaded. The text view is what you reach for to read a document as
31
+ * it is written, to select a slab of it, or to search it with the
32
+ * browser's own Ctrl+F; it shows the `__collapsed__` sentinels
33
+ * verbatim, because that is honestly what the client holds. The
34
+ * tree-only controls (search, expand/collapse) hide with the tree.
35
+ *
27
36
  * DOM is self-describing (UPPER_SNAKE logical classes): JSON_VIEWER /
28
- * JSON_TOOLBAR / JSON_SEARCH / JSON_TREE / JSON_ROW / JSON_KEY /
29
- * JSON_VALUE / JSON_SUMMARY / JSON_COLLAPSED / JSON_TIME.
37
+ * JSON_TOOLBAR / JSON_SEARCH / JSON_VIEW_MODE / JSON_TREE / JSON_TEXT /
38
+ * JSON_TEXT_BODY / JSON_ROW / JSON_KEY / JSON_VALUE / JSON_SUMMARY /
39
+ * JSON_COLLAPSED / JSON_TIME.
30
40
  *
31
41
  * Copyright (c) 2026, ArtGins.
32
42
  * All Rights Reserved.
@@ -62,6 +72,7 @@ import {
62
72
  subtree_matches,
63
73
  is_time_field,
64
74
  format_epoch,
75
+ json_text_dump,
65
76
  } from "./json_view_helpers.js";
66
77
 
67
78
  import {yui_toolbar} from "./yui_toolbar.js";
@@ -84,6 +95,14 @@ const GCLASS_NAME = "C_YUI_JSON";
84
95
  */
85
96
  const MAX_RENDER_ROWS = 5000;
86
97
 
98
+ /*
99
+ * Hard cap on characters painted by the text view. Same guard as
100
+ * MAX_RENDER_ROWS, one layer down: a document the tree renders lazily
101
+ * is dumped whole here, so the cap is announced under the text and
102
+ * never applied silently.
103
+ */
104
+ const MAX_TEXT_CHARS = 2000000;
105
+
87
106
  /***************************************************************
88
107
  * Data
89
108
  ***************************************************************/
@@ -94,6 +113,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outpu
94
113
  /*---------------- Config ----------------*/
95
114
  SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
96
115
  SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
116
+ SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree) or 'text' (raw JSON dump)"),
97
117
 
98
118
  /*---------------- UI ----------------*/
99
119
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
@@ -107,7 +127,13 @@ let PRIVATE_DATA = {
107
127
  errors: null, // Map<string,string> of per-path expand errors
108
128
  search: "", // current search term (lower-cased)
109
129
  $tree: null, // the scrollable tree body element
130
+ $text: null, // the scrollable text body element
131
+ $text_body: null, // the <pre> inside it
110
132
  $search: null, // the search input element
133
+ $search_ctl: null, // the search control (hidden in text view)
134
+ $expand_btn: null, // "expand loaded" button (hidden in text view)
135
+ $collapse_btn: null,// "collapse all" button (hidden in text view)
136
+ $mode_btn: null, // the tree/text switch
111
137
  };
112
138
 
113
139
  let __gclass__ = null;
@@ -140,6 +166,7 @@ function mt_create(gobj)
140
166
  }
141
167
 
142
168
  build_ui(gobj);
169
+ apply_view_mode(gobj);
143
170
 
144
171
  /*
145
172
  * CHILD subscription model
@@ -156,7 +183,7 @@ function mt_create(gobj)
156
183
  ***************************************************************/
157
184
  function mt_start(gobj)
158
185
  {
159
- render_tree(gobj);
186
+ render_view(gobj);
160
187
  }
161
188
 
162
189
  /***************************************************************
@@ -198,13 +225,20 @@ function build_ui(gobj)
198
225
  style: 'height:100%; display:flex; flex-direction:column;'}, [
199
226
  ['div', {class: 'JSON_TOOLBAR is-flex-grow-0'}, [$toolbar]],
200
227
  ['div', {class: 'JSON_TREE is-flex-grow-1',
228
+ style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
229
+ ['div', {class: 'JSON_TEXT is-flex-grow-1 is-hidden',
201
230
  style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []]
202
231
  ]]
203
232
  );
204
233
 
205
234
  gobj_write_attr(gobj, "$container", $container);
206
235
  priv.$tree = $container.querySelector('.JSON_TREE');
236
+ priv.$text = $container.querySelector('.JSON_TEXT');
207
237
  priv.$search = $container.querySelector('.JSON_SEARCH');
238
+ priv.$search_ctl = $container.querySelector('.JSON_SEARCH_CONTROL');
239
+ priv.$expand_btn = $container.querySelector('.EV_EXPAND_ALL');
240
+ priv.$collapse_btn = $container.querySelector('.EV_COLLAPSE_ALL');
241
+ priv.$mode_btn = $container.querySelector('.JSON_VIEW_MODE');
208
242
 
209
243
  refresh_language($container, t);
210
244
  }
@@ -223,11 +257,18 @@ function destroy_ui(gobj)
223
257
  gobj_write_attr(gobj, "$container", null);
224
258
  }
225
259
  priv.$tree = null;
260
+ priv.$text = null;
261
+ priv.$text_body = null;
226
262
  priv.$search = null;
263
+ priv.$search_ctl = null;
264
+ priv.$expand_btn = null;
265
+ priv.$collapse_btn = null;
266
+ priv.$mode_btn = null;
227
267
  }
228
268
 
229
269
  /************************************************************
230
- * Toolbar: search + expand-all / collapse-all / copy
270
+ * Toolbar: search + tree/text switch + expand-all /
271
+ * collapse-all / copy
231
272
  ************************************************************/
232
273
  function make_toolbar(gobj)
233
274
  {
@@ -251,7 +292,8 @@ function make_toolbar(gobj)
251
292
  }
252
293
  }]);
253
294
  let $search_control = createElement2(
254
- ['div', {class: 'control has-icons-left', style: 'max-width:22em;'}, [
295
+ ['div', {class: 'control has-icons-left JSON_SEARCH_CONTROL',
296
+ style: 'max-width:22em;'}, [
255
297
  $search_input,
256
298
  ['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
257
299
  ]]);
@@ -259,6 +301,7 @@ function make_toolbar(gobj)
259
301
  left_items.push($search_control);
260
302
 
261
303
  let right_items = [
304
+ view_mode_button(gobj),
262
305
  icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
263
306
  icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
264
307
  icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
@@ -293,6 +336,148 @@ function icon_button(gobj, icon, event_name, label_key)
293
336
  }];
294
337
  }
295
338
 
339
+ /************************************************************
340
+ * The tree/text switch.
341
+ *
342
+ * It shows the view it takes you TO — the `code` glyph while the
343
+ * tree is on screen, the `sitemap` glyph while the text is — and
344
+ * sends EV_SET_VIEW_MODE with no mode, which the action reads as
345
+ * "toggle". apply_view_mode() keeps the icon and its i18n keys in
346
+ * step with the attr.
347
+ ************************************************************/
348
+ function view_mode_button(gobj)
349
+ {
350
+ return ['button', {class: 'button JSON_VIEW_MODE', style: 'width:2.5em;',
351
+ title: t("text view"), 'data-i18n-title': "text view",
352
+ 'aria-label': t("text view"), 'data-i18n-aria-label': "text view"}, [
353
+ ['span', {class: 'icon'}, [['i', {class: 'yi-code'}]]]
354
+ ], {
355
+ click: function(evt) {
356
+ evt.stopPropagation();
357
+ gobj_send_event(gobj, "EV_SET_VIEW_MODE", {}, gobj);
358
+ }
359
+ }];
360
+ }
361
+
362
+ /************************************************************
363
+ * The current view, normalised. Anything but "text" is the tree.
364
+ ************************************************************/
365
+ function current_view_mode(gobj)
366
+ {
367
+ return (gobj_read_str_attr(gobj, "view_mode") === "text")? "text": "tree";
368
+ }
369
+
370
+ /************************************************************
371
+ * Show the chrome of the current view and hide the other's.
372
+ ************************************************************/
373
+ function apply_view_mode(gobj)
374
+ {
375
+ let priv = gobj.priv;
376
+ let text_mode = current_view_mode(gobj) === "text";
377
+
378
+ if(priv.$tree) {
379
+ priv.$tree.classList.toggle('is-hidden', text_mode);
380
+ }
381
+ if(priv.$text) {
382
+ priv.$text.classList.toggle('is-hidden', !text_mode);
383
+ }
384
+
385
+ /*
386
+ * Search and expand/collapse act on TREE rows; in the text view
387
+ * they have nothing to act on, and a control that answers nothing
388
+ * is worse than an absent one. A <pre> is searched with the
389
+ * browser's own Ctrl+F.
390
+ */
391
+ [priv.$search_ctl, priv.$expand_btn, priv.$collapse_btn].forEach(function($el) {
392
+ if($el) {
393
+ $el.classList.toggle('is-hidden', text_mode);
394
+ }
395
+ });
396
+
397
+ if(priv.$mode_btn) {
398
+ let icon = text_mode? "yi-sitemap": "yi-code";
399
+ /* Both keys spelled out inside t(), never t(variable): the apps'
400
+ * validate-locales scans for t("literal"), and a key it cannot
401
+ * see is a key it cannot demand — i18next then answers an
402
+ * undefined key with the key ITSELF, so the button renders in
403
+ * lower-case English and never changes language. `key` still
404
+ * carries the data-i18n-* attrs refresh_language() reads. */
405
+ let key = text_mode? "tree view": "text view";
406
+ let label = text_mode? t("tree view"): t("text view");
407
+ let $i = priv.$mode_btn.querySelector('i');
408
+ if($i) {
409
+ $i.className = icon;
410
+ }
411
+ priv.$mode_btn.setAttribute("data-i18n-title", key);
412
+ priv.$mode_btn.setAttribute("data-i18n-aria-label", key);
413
+ priv.$mode_btn.setAttribute("title", label);
414
+ priv.$mode_btn.setAttribute("aria-label", label);
415
+ }
416
+ }
417
+
418
+ /************************************************************
419
+ * Render whichever view is current.
420
+ ************************************************************/
421
+ function render_view(gobj)
422
+ {
423
+ if(current_view_mode(gobj) === "text") {
424
+ render_text(gobj);
425
+ } else {
426
+ render_tree(gobj);
427
+ }
428
+ }
429
+
430
+ /************************************************************
431
+ * Render the text view: the whole loaded document, verbatim.
432
+ *
433
+ * Unlike the tree, nothing here is lazy — what the client holds is
434
+ * what it prints, `__collapsed__` sentinels included. Over
435
+ * MAX_TEXT_CHARS the dump is cut and the cut is announced.
436
+ ************************************************************/
437
+ function render_text(gobj)
438
+ {
439
+ let priv = gobj.priv;
440
+ let $text = priv.$text;
441
+ if(!$text) {
442
+ return;
443
+ }
444
+
445
+ let scroll_top = $text.scrollTop;
446
+ $text.textContent = "";
447
+ priv.$text_body = null;
448
+
449
+ if(priv.root === null || priv.root === undefined) {
450
+ $text.appendChild(createElement2(
451
+ ['div', {class: 'JSON_EMPTY has-text-grey p-3', 'data-i18n': 'no data'}, 'no data']
452
+ ));
453
+ return;
454
+ }
455
+
456
+ let dump = json_text_dump(priv.root, MAX_TEXT_CHARS);
457
+ if(dump.error) {
458
+ log_error(`${GCLASS_NAME}: cannot dump the document as text: ${dump.error}`);
459
+ }
460
+
461
+ /* textContent, not a createElement2 child: the dump is raw text
462
+ * that must reach the DOM untouched, and createElement2 trims its
463
+ * text nodes. */
464
+ let $pre = createElement2(['pre', {class: 'JSON_TEXT_BODY'}, []]);
465
+ $pre.textContent = dump.text;
466
+ $text.appendChild($pre);
467
+ priv.$text_body = $pre;
468
+
469
+ if(dump.capped) {
470
+ $text.appendChild(createElement2(
471
+ ['div', {class: 'JSON_CAPPED has-text-warning p-2',
472
+ 'data-i18n': 'text truncated; collapse some branches'},
473
+ 'text truncated; collapse some branches']
474
+ ));
475
+ }
476
+
477
+ refresh_language($text, t);
478
+ $text.scrollTop = scroll_top;
479
+ }
480
+
296
481
  /************************************************************
297
482
  * Re-render the whole tree from priv.root + priv.expanded.
298
483
  *
@@ -629,7 +814,7 @@ function ac_set_json(gobj, event, kw, src)
629
814
  priv.pending.clear();
630
815
  priv.errors.clear();
631
816
 
632
- render_tree(gobj);
817
+ render_view(gobj);
633
818
  return 0;
634
819
  }
635
820
 
@@ -655,7 +840,7 @@ function ac_subtree_loaded(gobj, event, kw, src)
655
840
  priv.expanded.add(path); // reveal what we just loaded
656
841
  }
657
842
 
658
- render_tree(gobj);
843
+ render_view(gobj);
659
844
  return 0;
660
845
  }
661
846
 
@@ -672,7 +857,7 @@ function ac_subtree_error(gobj, event, kw, src)
672
857
 
673
858
  log_error(`${GCLASS_NAME}: subtree load failed at '${path}': ${kw.error || ""}`);
674
859
 
675
- render_tree(gobj);
860
+ render_view(gobj);
676
861
  return 0;
677
862
  }
678
863
 
@@ -690,7 +875,7 @@ function ac_toggle_node(gobj, event, kw, src)
690
875
  priv.expanded.add(path);
691
876
  }
692
877
 
693
- render_tree(gobj);
878
+ render_view(gobj);
694
879
  return 0;
695
880
  }
696
881
 
@@ -709,7 +894,7 @@ function ac_expand_collapsed(gobj, event, kw, src)
709
894
  priv.pending.add(path);
710
895
  priv.errors.delete(path);
711
896
 
712
- render_tree(gobj); // show the "loading…" state
897
+ render_view(gobj); // show the "loading…" state
713
898
 
714
899
  gobj_publish_event(gobj, "EV_EXPAND_PATH", {path: path, size: kw.size});
715
900
  return 0;
@@ -722,7 +907,7 @@ function ac_search(gobj, event, kw, src)
722
907
  {
723
908
  let priv = gobj.priv;
724
909
  priv.search = (kw.text || "").trim().toLowerCase();
725
- render_tree(gobj);
910
+ render_view(gobj);
726
911
  return 0;
727
912
  }
728
913
 
@@ -751,7 +936,7 @@ function ac_expand_all(gobj, event, kw, src)
751
936
  priv.expanded.add(p);
752
937
  });
753
938
 
754
- render_tree(gobj);
939
+ render_view(gobj);
755
940
  return 0;
756
941
  }
757
942
 
@@ -762,7 +947,7 @@ function ac_collapse_all(gobj, event, kw, src)
762
947
  {
763
948
  let priv = gobj.priv;
764
949
  priv.expanded.clear();
765
- render_tree(gobj);
950
+ render_view(gobj);
766
951
  return 0;
767
952
  }
768
953
 
@@ -775,7 +960,12 @@ function ac_copy_all(gobj, event, kw, src)
775
960
  if(priv.root === null || priv.root === undefined) {
776
961
  return 0;
777
962
  }
778
- let text = JSON.stringify(priv.root, null, 4);
963
+ let dump = json_text_dump(priv.root, 0);
964
+ if(dump.error) {
965
+ log_error(`${GCLASS_NAME}: cannot copy the document: ${dump.error}`);
966
+ return -1;
967
+ }
968
+ let text = dump.text;
779
969
  if(navigator.clipboard && navigator.clipboard.writeText) {
780
970
  navigator.clipboard.writeText(text).catch(function(e) {
781
971
  log_error(`${GCLASS_NAME}: clipboard write failed: ${e}`);
@@ -786,6 +976,29 @@ function ac_copy_all(gobj, event, kw, src)
786
976
  return 0;
787
977
  }
788
978
 
979
+ /************************************************************
980
+ * EV_SET_VIEW_MODE { mode } — "tree" | "text".
981
+ *
982
+ * With no mode it toggles, which is what the toolbar button sends.
983
+ ************************************************************/
984
+ function ac_set_view_mode(gobj, event, kw, src)
985
+ {
986
+ let mode = kw.mode;
987
+
988
+ if(mode === undefined || mode === null || mode === "") {
989
+ mode = (current_view_mode(gobj) === "text")? "tree": "text";
990
+ } else if(mode !== "tree" && mode !== "text") {
991
+ log_error(`${GCLASS_NAME}: unknown view_mode '${mode}'`);
992
+ return -1;
993
+ }
994
+
995
+ gobj_write_attr(gobj, "view_mode", mode);
996
+
997
+ apply_view_mode(gobj);
998
+ render_view(gobj);
999
+ return 0;
1000
+ }
1001
+
789
1002
  /************************************************************
790
1003
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
791
1004
  ************************************************************/
@@ -798,8 +1011,9 @@ function ac_language_changed(gobj, event, kw, src)
798
1011
  if(priv.$search) {
799
1012
  priv.$search.setAttribute("placeholder", t("search"));
800
1013
  }
1014
+ apply_view_mode(gobj);
801
1015
  }
802
- render_tree(gobj);
1016
+ render_view(gobj);
803
1017
  return 0;
804
1018
  }
805
1019
 
@@ -829,7 +1043,7 @@ function ac_hide(gobj, event, kw, src)
829
1043
  ************************************************************/
830
1044
  function ac_refresh(gobj, event, kw, src)
831
1045
  {
832
- render_tree(gobj);
1046
+ render_view(gobj);
833
1047
  return 0;
834
1048
  }
835
1049
 
@@ -869,6 +1083,7 @@ function create_gclass(gclass_name)
869
1083
  const states = [
870
1084
  ["ST_EMPTY", [
871
1085
  ["EV_SET_JSON", ac_set_json, "ST_READY"],
1086
+ ["EV_SET_VIEW_MODE", ac_set_view_mode, null],
872
1087
  ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
873
1088
  ["EV_REFRESH", ac_refresh, null],
874
1089
  ["EV_SHOW", ac_show, null],
@@ -884,6 +1099,7 @@ function create_gclass(gclass_name)
884
1099
  ["EV_EXPAND_ALL", ac_expand_all, null],
885
1100
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
886
1101
  ["EV_COPY_ALL", ac_copy_all, null],
1102
+ ["EV_SET_VIEW_MODE", ac_set_view_mode, null],
887
1103
  ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
888
1104
  ["EV_REFRESH", ac_refresh, null],
889
1105
  ["EV_SHOW", ac_show, null],
@@ -904,6 +1120,7 @@ function create_gclass(gclass_name)
904
1120
  ["EV_EXPAND_ALL", 0],
905
1121
  ["EV_COLLAPSE_ALL", 0],
906
1122
  ["EV_COPY_ALL", 0],
1123
+ ["EV_SET_VIEW_MODE", 0],
907
1124
  ["EV_LANGUAGE_CHANGED", 0],
908
1125
  ["EV_REFRESH", 0],
909
1126
  ["EV_SHOW", 0],
@@ -4,8 +4,8 @@
4
4
  * Pure, testable logic behind C_YUI_JSON (the lazy JSON tree
5
5
  * viewer). Kept out of the gclass so it can be unit-tested with
6
6
  * no DOM: collapsed-sentinel detection, path (segments) algebra,
7
- * search matching, timestamp recognition/formatting and the JSON
8
- * type discriminator.
7
+ * search matching, timestamp recognition/formatting, the JSON
8
+ * type discriminator and the text view's capped dump.
9
9
  *
10
10
  * Path convention mirrors the C kernel (kw_collapse / kw_find_path
11
11
  * in kwid.c): segments are joined by the backtick delimiter, arrays
@@ -212,3 +212,33 @@ export function format_epoch(value)
212
212
  }
213
213
  return d.toLocaleString();
214
214
  }
215
+
216
+
217
+ /************************************************************
218
+ * The text view's payload: the document as text, indented
219
+ * four characters, cut at `max_chars`.
220
+ *
221
+ * Returns {text, capped}. `capped` is what the caller
222
+ * announces under the dump — a cut that is not announced
223
+ * reads as a document that ends there.
224
+ ************************************************************/
225
+ export function json_text_dump(root, max_chars)
226
+ {
227
+ let text;
228
+ try {
229
+ text = JSON.stringify(root, null, 4);
230
+ } catch(e) {
231
+ return {text: "", capped: false, error: String(e)};
232
+ }
233
+
234
+ if(text === undefined) {
235
+ /* JSON.stringify() answers undefined for a value that has no
236
+ * JSON form at all (a function, a bare undefined). */
237
+ return {text: "", capped: false, error: "not representable as JSON"};
238
+ }
239
+
240
+ if(typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) {
241
+ return {text: text.slice(0, max_chars), capped: true};
242
+ }
243
+ return {text: text, capped: false};
244
+ }
@@ -15,6 +15,7 @@ import {
15
15
  subtree_matches,
16
16
  is_time_field,
17
17
  format_epoch,
18
+ json_text_dump,
18
19
  } from "./json_view_helpers.js";
19
20
 
20
21
 
@@ -133,3 +134,44 @@ test("format_epoch handles seconds, milliseconds and the unset case", () => {
133
134
  expect(typeof format_epoch(1700000000)).toBe("string"); // seconds
134
135
  expect(typeof format_epoch(1700000000000)).toBe("string"); // milliseconds
135
136
  });
137
+
138
+
139
+ /*============================================================
140
+ * text view dump
141
+ *============================================================*/
142
+ test("json_text_dump indents four characters, like every other tree", () => {
143
+ let d = json_text_dump({a: {b: 1}}, 0);
144
+ expect(d.capped).toBe(false);
145
+ expect(d.error).toBeUndefined();
146
+ expect(d.text).toBe('{\n "a": {\n "b": 1\n }\n}');
147
+ });
148
+
149
+ test("json_text_dump prints a collapsed sentinel verbatim", () => {
150
+ let d = json_text_dump({rows: {__collapsed__: {path: "rows", size: 900}}}, 0);
151
+ expect(d.text).toContain("__collapsed__");
152
+ expect(d.text).toContain("900");
153
+ });
154
+
155
+ test("json_text_dump cuts at max_chars and says so", () => {
156
+ let d = json_text_dump({a: "x".repeat(500)}, 100);
157
+ expect(d.capped).toBe(true);
158
+ expect(d.text.length).toBe(100);
159
+ });
160
+
161
+ test("json_text_dump does not cap when max_chars is absent or zero", () => {
162
+ let big = {a: "x".repeat(500)};
163
+ expect(json_text_dump(big, 0).capped).toBe(false);
164
+ expect(json_text_dump(big).capped).toBe(false);
165
+ });
166
+
167
+ test("json_text_dump reports a document with no JSON form instead of printing nothing", () => {
168
+ let circular = {};
169
+ circular.self = circular;
170
+ let d = json_text_dump(circular, 0);
171
+ expect(d.text).toBe("");
172
+ expect(typeof d.error).toBe("string");
173
+
174
+ let d2 = json_text_dump(undefined, 0);
175
+ expect(d2.text).toBe("");
176
+ expect(typeof d2.error).toBe("string");
177
+ });
package/src/yui_icons.css CHANGED
@@ -363,3 +363,16 @@
363
363
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z'/%3E%3C/svg%3E");
364
364
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z'/%3E%3C/svg%3E");
365
365
  }
366
+
367
+ /* The two faces of the JSON viewer's view switch: the tree it renders
368
+ * and the raw text behind it. Neither was in the set, and a missing
369
+ * glyph renders as a solid black square. */
370
+ .yi-code::before {
371
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath d='M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z'/%3E%3C/svg%3E");
372
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath d='M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z'/%3E%3C/svg%3E");
373
+ }
374
+
375
+ .yi-sitemap::before {
376
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M208 80c0-26.5 21.5-48 48-48l64 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-8 0 0 40 152 0c30.9 0 56 25.1 56 56l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-4.4-3.6-8-8-8l-152 0 0 40 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-40-152 0c-4.4 0-8 3.6-8 8l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-30.9 25.1-56 56-56l152 0 0-40-8 0c-26.5 0-48-21.5-48-48l0-64z'/%3E%3C/svg%3E");
377
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M208 80c0-26.5 21.5-48 48-48l64 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-8 0 0 40 152 0c30.9 0 56 25.1 56 56l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-4.4-3.6-8-8-8l-152 0 0 40 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-40-152 0c-4.4 0-8 3.6-8 8l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-30.9 25.1-56 56-56l152 0 0-40-8 0c-26.5 0-48-21.5-48-48l0-64z'/%3E%3C/svg%3E");
378
+ }