@yuneta/gobj-ui 7.23.35 → 7.23.37

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.
@@ -743,10 +743,10 @@ function build_popover(gobj, $container)
743
743
 
744
744
  /* What this gobj IS: its gclass, opened in the gclass viewer.
745
745
  *
746
- * Built only where the app registers C_YUI_JSON -- the viewer
747
- * the window hosts. An app that does not carry it gets no
748
- * button rather than a button that fails, and the reason is
749
- * logged once, at build time. */
746
+ * The viewer registers itself, so this is offered everywhere --
747
+ * but it is still asked, because a registration that failed
748
+ * must not leave a button that opens nothing, and the reason is
749
+ * then logged once, at build time. */
750
750
  if(gclass_view_available()) {
751
751
  let $gclass = document.createElement("button");
752
752
  $gclass.type = "button";
@@ -772,8 +772,8 @@ function build_popover(gobj, $container)
772
772
  priv.$popover_gclass = $gclass;
773
773
  } else {
774
774
  log_warning(
775
- `${gobj_short_name(gobj)}: no C_YUI_JSON registered, ` +
776
- `the gclass viewer is not offered`
775
+ `${gobj_short_name(gobj)}: no gclass viewer registered, ` +
776
+ `the control is not offered`
777
777
  );
778
778
  }
779
779
 
@@ -2603,6 +2603,11 @@ function ac_open_gclass(gobj, event, kw, src)
2603
2603
 
2604
2604
  priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
2605
2605
  title_prefix: gobj_yuno_name() || "",
2606
+ /* Which state THIS gobj is in. The description describes the
2607
+ * CLASS and cannot carry it, and the tree is the only party
2608
+ * that knows -- so the machine lights the column the reader
2609
+ * is actually standing in. */
2610
+ current_state: node_data.state || "",
2606
2611
  on_close: () => {
2607
2612
  if(gobj_is_destroying(gobj)) {
2608
2613
  return;
@@ -2627,25 +2632,6 @@ function ac_gclass_closed(gobj, event, kw, src)
2627
2632
  return 0;
2628
2633
  }
2629
2634
 
2630
- /************************************************************
2631
- * The gclass viewer asked for a subtree.
2632
- *
2633
- * It cannot: the gclass description is a COMPLETE document,
2634
- * it carries no `__collapsed__` stub, so nothing in it is
2635
- * fetchable. The event is declared because the viewer is
2636
- * hosted as a child and subscribes its host to everything it
2637
- * publishes -- and it is logged, because arriving here means
2638
- * the document was not the one we built.
2639
- ************************************************************/
2640
- function ac_gclass_expand_path(gobj, event, kw, src)
2641
- {
2642
- log_error(
2643
- `${gobj_short_name(gobj)}: EV_EXPAND_PATH on a complete document: ` +
2644
- `${kw && kw.path}`
2645
- );
2646
- return -1;
2647
- }
2648
-
2649
2635
  /************************************************************
2650
2636
  * The app switched language.
2651
2637
  *
@@ -2716,7 +2702,6 @@ function create_gclass(gclass_name)
2716
2702
  ["EV_HIDE", ac_hide, null],
2717
2703
  ["EV_OPEN_GCLASS", ac_open_gclass, null],
2718
2704
  ["EV_GCLASS_CLOSED", ac_gclass_closed, null],
2719
- ["EV_EXPAND_PATH", ac_gclass_expand_path, null],
2720
2705
  ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
2721
2706
  ]]
2722
2707
  ];
@@ -2743,7 +2728,6 @@ function create_gclass(gclass_name)
2743
2728
  ["EV_HIDE", 0],
2744
2729
  ["EV_OPEN_GCLASS", 0],
2745
2730
  ["EV_GCLASS_CLOSED", 0],
2746
- ["EV_EXPAND_PATH", 0],
2747
2731
  ["EV_LANGUAGE_CHANGED", 0],
2748
2732
  ];
2749
2733
 
@@ -0,0 +1,421 @@
1
+ /***********************************************************************
2
+ * gclass_view_model.js
3
+ *
4
+ * The description of a gclass, turned into what a viewer
5
+ * draws: zones, a matrix, and the states nothing declares a
6
+ * way into.
7
+ *
8
+ * Pure data, no DOM -- so the shape of the view is testable
9
+ * without a browser, and the gclass that draws it has nothing
10
+ * to decide.
11
+ *
12
+ * IT READS TWO DIALECTS. The C kernel (`gclass2json()` in
13
+ * `kernel/c/gobj-c/src/gobj.c`) and the browser registry
14
+ * (`gclass_describe.js`) answer the same DOCUMENT in
15
+ * different words, and a viewer that only knew one of them
16
+ * would draw half a page for the other side:
17
+ *
18
+ * key C kernel browser
19
+ * ------------------------------------------------------
20
+ * flag "SDF_RD|SDF_PERSIST" ["SDF_RD", ...]
21
+ * type "string" "DTP_STRING"
22
+ * command "command" "id"
23
+ * parameter "parameter" "id"
24
+ * traces "info_gclass_trace" "trace_levels"
25
+ * action "action" (a marker) the function's name
26
+ *
27
+ * The last one is not a rename: the C side cannot name an
28
+ * action at all -- `states2json()` writes the literal string
29
+ * "action" for every one of them. So a cell coming from a
30
+ * backend gclass says THAT there is an action and never which,
31
+ * and the model reports the difference instead of printing a
32
+ * name nobody wrote.
33
+ *
34
+ * Copyright (c) 2026, ArtGins.
35
+ * All Rights Reserved.
36
+ ***********************************************************************/
37
+
38
+ /*
39
+ * What the C side writes where a function name would go. It is not a
40
+ * name -- every action in every backend gclass carries this one.
41
+ */
42
+ const C_ANONYMOUS_ACTION = "action";
43
+
44
+
45
+ /************************************************************
46
+ * Flags, from either dialect, as a list of names.
47
+ *
48
+ * The C side joins them with '|' (`bits2gbuffer`), the
49
+ * browser side keeps the array `bits2names()` built.
50
+ ************************************************************/
51
+ function normalize_flags(flag)
52
+ {
53
+ if(Array.isArray(flag)) {
54
+ return flag.filter((f) => typeof f === "string" && f.length > 0);
55
+ }
56
+ if(typeof flag === "string" && flag.length > 0) {
57
+ return flag.split("|").map((f) => f.trim()).filter((f) => f.length > 0);
58
+ }
59
+ return [];
60
+ }
61
+
62
+ /************************************************************
63
+ * A data type, short and lower case, from either dialect.
64
+ *
65
+ * An unknown type is reported as what it is: a viewer that
66
+ * silently drew "string" for something nobody registered
67
+ * would be lying about the schema.
68
+ ************************************************************/
69
+ function normalize_type(type)
70
+ {
71
+ if(typeof type === "string" && type.length > 0) {
72
+ if(type.startsWith("DTP_")) {
73
+ return type.slice(4).toLowerCase();
74
+ }
75
+ return type.toLowerCase();
76
+ }
77
+ if(type === null || type === undefined) {
78
+ return "";
79
+ }
80
+ return String(type);
81
+ }
82
+
83
+ /************************************************************
84
+ * A default value as one short line.
85
+ *
86
+ * The C side stringifies everything, the browser side keeps
87
+ * the live value -- including `null` for a pointer, which
88
+ * prints as an empty cell unless it is spelled out.
89
+ ************************************************************/
90
+ function normalize_default(value)
91
+ {
92
+ if(value === undefined) {
93
+ return "";
94
+ }
95
+ if(value === null) {
96
+ return "null";
97
+ }
98
+ if(typeof value === "string") {
99
+ return value;
100
+ }
101
+ if(typeof value === "number" || typeof value === "boolean") {
102
+ return String(value);
103
+ }
104
+ try {
105
+ return JSON.stringify(value);
106
+ } catch(e) {
107
+ return String(value);
108
+ }
109
+ }
110
+
111
+ /************************************************************
112
+ * One attr / parameter row, from either dialect.
113
+ ************************************************************/
114
+ function normalize_field(row)
115
+ {
116
+ if(!row || typeof row !== "object") {
117
+ return null;
118
+ }
119
+
120
+ let id = row.id || row.parameter || row.name || "";
121
+ if(!id) {
122
+ return null;
123
+ }
124
+
125
+ return {
126
+ id: id,
127
+ type: normalize_type(row.type),
128
+ flags: normalize_flags(row.flag),
129
+ default_value: normalize_default(row.default_value),
130
+ description: row.description || "",
131
+ };
132
+ }
133
+
134
+ /************************************************************
135
+ * The attrs zone.
136
+ ************************************************************/
137
+ function normalize_attrs(attrs)
138
+ {
139
+ if(!Array.isArray(attrs)) {
140
+ return [];
141
+ }
142
+ let rows = [];
143
+ for(let it of attrs) {
144
+ let row = normalize_field(it);
145
+ if(row) {
146
+ rows.push(row);
147
+ }
148
+ }
149
+ return rows;
150
+ }
151
+
152
+ /************************************************************
153
+ * The commands zone.
154
+ *
155
+ * A command with no name is not a command: the C tables use
156
+ * `SDATACM(DTP_SCHEMA, "", ...)` rows as SECTION HEADINGS,
157
+ * carrying the heading in the description (c_iogate, c_agent
158
+ * and every large gclass do it). Dropping them would glue
159
+ * four unrelated groups of commands into one list; drawing
160
+ * them as commands would offer the reader a nameless one.
161
+ ************************************************************/
162
+ function normalize_commands(commands)
163
+ {
164
+ if(!Array.isArray(commands)) {
165
+ return [];
166
+ }
167
+
168
+ let rows = [];
169
+ for(let it of commands) {
170
+ if(!it || typeof it !== "object") {
171
+ continue;
172
+ }
173
+
174
+ let id = it.command || it.id || it.name || "";
175
+ if(!id) {
176
+ let label = String(it.description || "")
177
+ .split("\n")
178
+ .map((s) => s.trim())
179
+ .filter((s) => s.length > 0 && !/^[-=_]+$/.test(s))[0] || "";
180
+ if(label) {
181
+ rows.push({kind: "section", label: label});
182
+ }
183
+ continue;
184
+ }
185
+
186
+ let parameters = [];
187
+ for(let p of (Array.isArray(it.parameters)? it.parameters:
188
+ (Array.isArray(it.schema)? it.schema: []))) {
189
+ let param = normalize_field(p);
190
+ if(param) {
191
+ parameters.push(param);
192
+ }
193
+ }
194
+
195
+ rows.push({
196
+ kind: "command",
197
+ id: id,
198
+ alias: Array.isArray(it.alias)? it.alias.slice(): [],
199
+ flags: normalize_flags(it.flag),
200
+ description: it.description || "",
201
+ usage: it.usage || "",
202
+ parameters: parameters,
203
+ });
204
+ }
205
+
206
+ return rows;
207
+ }
208
+
209
+ /************************************************************
210
+ * The trace levels, from either dialect.
211
+ *
212
+ * C answers a dict {name: description} (`gobj_trace_level_list`),
213
+ * the browser hands over the `s_user_trace_level` array.
214
+ ************************************************************/
215
+ function normalize_trace_levels(description)
216
+ {
217
+ let src = description.trace_levels;
218
+ if(src === undefined || src === null) {
219
+ src = description.info_gclass_trace;
220
+ }
221
+
222
+ let rows = [];
223
+ if(Array.isArray(src)) {
224
+ for(let it of src) {
225
+ if(!it) {
226
+ continue;
227
+ }
228
+ if(typeof it === "string") {
229
+ rows.push({id: it, description: ""});
230
+ } else if(it.name || it.id) {
231
+ rows.push({id: it.name || it.id, description: it.description || ""});
232
+ }
233
+ }
234
+ } else if(src && typeof src === "object") {
235
+ for(let key of Object.keys(src)) {
236
+ rows.push({id: key, description: String(src[key] || "")});
237
+ }
238
+ }
239
+ return rows;
240
+ }
241
+
242
+ /************************************************************
243
+ * A list of method names, defensively.
244
+ ************************************************************/
245
+ function normalize_methods(methods)
246
+ {
247
+ if(!Array.isArray(methods)) {
248
+ return [];
249
+ }
250
+ return methods.filter((m) => typeof m === "string" && m.length > 0);
251
+ }
252
+
253
+ /************************************************************
254
+ * One cell of the matrix, or null where the state declares
255
+ * nothing for that event.
256
+ *
257
+ * `has_action` and `action` are separate on purpose: a
258
+ * backend gclass has actions it cannot name.
259
+ ************************************************************/
260
+ function normalize_cell(entry)
261
+ {
262
+ let action = (typeof entry.action === "string")? entry.action: "";
263
+ let named = (action.length > 0 && action !== C_ANONYMOUS_ACTION);
264
+
265
+ return {
266
+ action: named? action: "",
267
+ has_action: action.length > 0,
268
+ next_state: entry.next_state || "",
269
+ };
270
+ }
271
+
272
+ /************************************************************
273
+ * The machine: a matrix, the events nobody handles, and the
274
+ * states nothing declares a way into.
275
+ *
276
+ * ROWS ARE EVENTS AND COLUMNS ARE STATES, not the other way
277
+ * round: a gclass almost always declares more events than
278
+ * states (12 against 3 in C_IEVENT_CLI, 86 commands and one
279
+ * state in c_agent), and a table grows better downwards than
280
+ * sideways.
281
+ ************************************************************/
282
+ function build_fsm(fsm)
283
+ {
284
+ fsm = fsm || {};
285
+
286
+ let states_src = (fsm.states && typeof fsm.states === "object")? fsm.states: {};
287
+ let state_names = Object.keys(states_src);
288
+
289
+ /*
290
+ * Every event, in DECLARATION order -- which is the order the
291
+ * gclass author chose, and the only one that carries meaning.
292
+ * An event handled by a state but missing from the table is a
293
+ * defect in that gclass; it is appended rather than dropped, so
294
+ * the viewer shows it instead of hiding it.
295
+ */
296
+ let order = [];
297
+ let flags_of = new Map();
298
+ for(let ev of (Array.isArray(fsm.events)? fsm.events: [])) {
299
+ if(!ev || !ev.event_name) {
300
+ continue;
301
+ }
302
+ if(!flags_of.has(ev.event_name)) {
303
+ order.push(ev.event_name);
304
+ }
305
+ flags_of.set(ev.event_name, normalize_flags(ev.event_flag));
306
+ }
307
+
308
+ let cells_of = new Map(); // event -> Map<state, cell>
309
+ let incoming = new Set(); // states some transition names
310
+ let counts = new Map(); // state -> how many events it handles
311
+
312
+ for(let state of state_names) {
313
+ let actions = Array.isArray(states_src[state])? states_src[state]: [];
314
+ counts.set(state, actions.length);
315
+
316
+ for(let entry of actions) {
317
+ if(!entry || !entry.event_name) {
318
+ continue;
319
+ }
320
+ if(!flags_of.has(entry.event_name)) {
321
+ order.push(entry.event_name);
322
+ flags_of.set(entry.event_name, []);
323
+ }
324
+ if(!cells_of.has(entry.event_name)) {
325
+ cells_of.set(entry.event_name, new Map());
326
+ }
327
+ let cell = normalize_cell(entry);
328
+ cells_of.get(entry.event_name).set(state, cell);
329
+ if(cell.next_state) {
330
+ incoming.add(cell.next_state);
331
+ }
332
+ }
333
+ }
334
+
335
+ let rows = [];
336
+ let published = [];
337
+ for(let event of order) {
338
+ let by_state = cells_of.get(event);
339
+ if(!by_state || by_state.size === 0) {
340
+ /*
341
+ * Declared and handled nowhere: an OUTPUT event. It has no
342
+ * place in a matrix -- it is not handled, it is published --
343
+ * and leaving it in would draw one empty row per output
344
+ * event across every column.
345
+ */
346
+ published.push({event: event, flags: flags_of.get(event) || []});
347
+ continue;
348
+ }
349
+ rows.push({
350
+ event: event,
351
+ flags: flags_of.get(event) || [],
352
+ cells: state_names.map((state) => by_state.get(state) || null),
353
+ });
354
+ }
355
+
356
+ /*
357
+ * A state nothing declares a way into. The first state is where
358
+ * the machine starts, so it needs none.
359
+ *
360
+ * This is NOT always a defect: an action is free to jump with
361
+ * `gobj_change_state()`, and several do (C_IEVENT_CLI reaches
362
+ * ST_SESSION that way). The description cannot see inside an
363
+ * action, so the viewer marks the state instead of drawing it
364
+ * as dead -- the alternative is a picture in which the working
365
+ * half of a gclass looks unreachable.
366
+ */
367
+ let unreachable = state_names.filter(
368
+ (state, i) => i > 0 && !incoming.has(state)
369
+ );
370
+
371
+ return {
372
+ entry_state: state_names.length? state_names[0]: "",
373
+ states: state_names.map((name) => ({
374
+ name: name,
375
+ count: counts.get(name) || 0,
376
+ })),
377
+ rows: rows,
378
+ published: published,
379
+ unreachable: unreachable,
380
+ };
381
+ }
382
+
383
+ /************************************************************
384
+ * The whole model.
385
+ *
386
+ * A missing key is an EMPTY zone, never a thrown viewer: the
387
+ * two dialects do not carry the same keys, and a description
388
+ * can also arrive from an old backend.
389
+ ************************************************************/
390
+ function gclass_view_model(description)
391
+ {
392
+ if(!description || typeof description !== "object") {
393
+ return null;
394
+ }
395
+
396
+ let instances = description.instances;
397
+
398
+ return {
399
+ id: description.id || "",
400
+ gcflag: normalize_flags(description.gcflag),
401
+ instances: (typeof instances === "number")? instances: null,
402
+ priv_size: (typeof description.priv_size === "number")?
403
+ description.priv_size: null,
404
+ attrs: normalize_attrs(description.attrs),
405
+ commands: normalize_commands(description.commands),
406
+ methods: normalize_methods(description.gclass_methods),
407
+ internal_methods: normalize_methods(description.internal_methods),
408
+ trace_levels: normalize_trace_levels(description),
409
+ fsm: build_fsm(description.FSM),
410
+ };
411
+ }
412
+
413
+ export {
414
+ normalize_flags,
415
+ normalize_type,
416
+ normalize_default,
417
+ normalize_commands,
418
+ normalize_trace_levels,
419
+ build_fsm,
420
+ gclass_view_model,
421
+ };