@yuneta/gobj-ui 7.23.36 → 7.23.38
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/README.md +14 -2
- package/dist/gobj-ui.cjs.js +35 -3
- package/dist/gobj-ui.es.js +35 -3
- package/package.json +1 -1
- package/src/c_yui_fsm_graph.js +50 -2
- package/src/c_yui_gclass.css +16 -0
- package/src/c_yui_gclass.js +25 -1
- package/src/gclass_view_model.test.js +14 -0
- package/src/yui_gclass_view.js +23 -0
package/README.md
CHANGED
|
@@ -1433,6 +1433,11 @@ description describes the CLASS and cannot carry it, so the host supplies it —
|
|
|
1433
1433
|
the gobj tree passes `node_data.state` — and the machine lights that column.
|
|
1434
1434
|
Omitted, nothing is lit.
|
|
1435
1435
|
|
|
1436
|
+
`close_gclass_view(handle)` is the host-driven teardown, and the ✕ runs it too:
|
|
1437
|
+
the window destroys itself, but the viewer hangs from the HOST, so nothing else
|
|
1438
|
+
would take it down — and a viewer left alive keeps its service name, which made
|
|
1439
|
+
the next click answer *"service ALREADY registered"* (fixed in 7.23.38).
|
|
1440
|
+
|
|
1436
1441
|
`gclass_view_available()` registers `C_YUI_GCLASS` on demand and reports
|
|
1437
1442
|
whether it is there, so no app has to mount a gclass for a control it never
|
|
1438
1443
|
asked for. The gobj tree still asks before drawing its `gclass` button: a
|
|
@@ -1467,7 +1472,10 @@ for the other side:
|
|
|
1467
1472
|
|
|
1468
1473
|
The last one is not a rename: `states2json()` cannot name an action, so a cell
|
|
1469
1474
|
from a **backend** gclass says THAT there is one and never which. The viewer
|
|
1470
|
-
draws a mark rather than printing a name nobody wrote
|
|
1475
|
+
draws a mark rather than printing a name nobody wrote — and an em dash for the
|
|
1476
|
+
third case, an event declared with NO action (`{EV_TX_READY, 0, 0}`, which
|
|
1477
|
+
C_WEBSOCKET has in all four states): legal in that state, and deliberately
|
|
1478
|
+
doing nothing.
|
|
1471
1479
|
|
|
1472
1480
|
**The machine is a matrix**, rows = events and columns = states — the shape the
|
|
1473
1481
|
FSM is declared in, and the one that survives 12 events against 3 states as
|
|
@@ -1507,7 +1515,11 @@ What it refuses to draw is the interesting half:
|
|
|
1507
1515
|
- **One edge per PAIR of states**, not per transition, labelled with the events
|
|
1508
1516
|
that make the jump (three, then a count). G6 draws overlapping edges on top
|
|
1509
1517
|
of each other, so a second edge between the same two states is invisible and
|
|
1510
|
-
nothing tells the reader it is there.
|
|
1518
|
+
nothing tells the reader it is there. A RECIPROCAL pair (`A -> B` and
|
|
1519
|
+
`B -> A`) is bowed apart into two arcs — with the SAME `curveOffset` on both,
|
|
1520
|
+
because G6 measures it from each edge's own direction, and with the type
|
|
1521
|
+
varied through a FUNCTION on `edge.type`, because a `type` written on the
|
|
1522
|
+
datum is overridden by the graph-level default.
|
|
1511
1523
|
- **An event a state handles without leaving is not an edge.** Ten self-loops
|
|
1512
1524
|
on one card say nothing a number cannot say better, so the card carries
|
|
1513
1525
|
`n events · m leave` instead.
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -14010,7 +14010,7 @@ function build_graph$3(gobj) {
|
|
|
14010
14010
|
zoomRange: [.2, 3],
|
|
14011
14011
|
padding: 24,
|
|
14012
14012
|
edge: {
|
|
14013
|
-
type: "polyline",
|
|
14013
|
+
type: (d) => d && d.data && d.data.reciprocal ? "quadratic" : "polyline",
|
|
14014
14014
|
style: {
|
|
14015
14015
|
stroke: edge_color(),
|
|
14016
14016
|
lineWidth: 1.2,
|
|
@@ -14151,11 +14151,18 @@ function build_data(gobj) {
|
|
|
14151
14151
|
let shown = pair.events.slice(0, MAX_EDGE_LABELS);
|
|
14152
14152
|
let label = shown.join("\n");
|
|
14153
14153
|
if (pair.events.length > shown.length) label += "\n+" + (pair.events.length - shown.length);
|
|
14154
|
+
let style = { labelText: label };
|
|
14155
|
+
let reciprocal = by_pair.has(pair.target + " " + pair.source);
|
|
14156
|
+
if (reciprocal) {
|
|
14157
|
+
style.curveOffset = 34;
|
|
14158
|
+
style.labelPlacement = .5;
|
|
14159
|
+
}
|
|
14154
14160
|
edges.push({
|
|
14155
14161
|
id: "e_" + edges.length,
|
|
14156
14162
|
source: pair.source,
|
|
14157
14163
|
target: pair.target,
|
|
14158
|
-
|
|
14164
|
+
data: { reciprocal },
|
|
14165
|
+
style
|
|
14159
14166
|
});
|
|
14160
14167
|
}
|
|
14161
14168
|
return {
|
|
@@ -15542,7 +15549,7 @@ function build_machine(gobj, fsm) {
|
|
|
15542
15549
|
{ class: "GCLASS_MX_ACTION" },
|
|
15543
15550
|
cell.action
|
|
15544
15551
|
]);
|
|
15545
|
-
else items.push([
|
|
15552
|
+
else if (cell.has_action) items.push([
|
|
15546
15553
|
"span",
|
|
15547
15554
|
{
|
|
15548
15555
|
class: "GCLASS_MX_ACTION is-unnamed",
|
|
@@ -15551,6 +15558,15 @@ function build_machine(gobj, fsm) {
|
|
|
15551
15558
|
},
|
|
15552
15559
|
"●"
|
|
15553
15560
|
]);
|
|
15561
|
+
else items.push([
|
|
15562
|
+
"span",
|
|
15563
|
+
{
|
|
15564
|
+
class: "GCLASS_MX_ACTION is-none-action",
|
|
15565
|
+
title: (0, i18next.t)("accepted with no action"),
|
|
15566
|
+
"data-i18n-title": "accepted with no action"
|
|
15567
|
+
},
|
|
15568
|
+
"—"
|
|
15569
|
+
]);
|
|
15554
15570
|
if (cell.next_state) {
|
|
15555
15571
|
items.push([
|
|
15556
15572
|
"span",
|
|
@@ -15679,6 +15695,19 @@ function build_legend(fsm, current) {
|
|
|
15679
15695
|
(0, i18next.t)("not declared in this state")
|
|
15680
15696
|
]]
|
|
15681
15697
|
]);
|
|
15698
|
+
if (fsm.rows.some((row) => row.cells.some((cell) => cell && !cell.has_action))) items.push([
|
|
15699
|
+
"span",
|
|
15700
|
+
{ class: "GCLASS_LEGEND_ITEM" },
|
|
15701
|
+
[[
|
|
15702
|
+
"i",
|
|
15703
|
+
{ class: "GCLASS_LEGEND_SWATCH is-none-action" },
|
|
15704
|
+
"—"
|
|
15705
|
+
], [
|
|
15706
|
+
"span",
|
|
15707
|
+
{ i18n: "accepted with no action" },
|
|
15708
|
+
(0, i18next.t)("accepted with no action")
|
|
15709
|
+
]]
|
|
15710
|
+
]);
|
|
15682
15711
|
if (current && fsm.states.some((s) => s.name === current)) items.push([
|
|
15683
15712
|
"span",
|
|
15684
15713
|
{ class: "GCLASS_LEGEND_ITEM" },
|
|
@@ -16529,6 +16558,9 @@ function open_gclass_view(host, gclass_name, opts) {
|
|
|
16529
16558
|
};
|
|
16530
16559
|
let $box = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(jv, "$container");
|
|
16531
16560
|
let on_close = () => {
|
|
16561
|
+
handle.win = null;
|
|
16562
|
+
handle.modal = null;
|
|
16563
|
+
close_gclass_view(handle);
|
|
16532
16564
|
if (typeof opts.on_close === "function") opts.on_close();
|
|
16533
16565
|
};
|
|
16534
16566
|
if (is_mobile$3()) {
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -14005,7 +14005,7 @@ function build_graph$3(gobj) {
|
|
|
14005
14005
|
zoomRange: [.2, 3],
|
|
14006
14006
|
padding: 24,
|
|
14007
14007
|
edge: {
|
|
14008
|
-
type: "polyline",
|
|
14008
|
+
type: (d) => d && d.data && d.data.reciprocal ? "quadratic" : "polyline",
|
|
14009
14009
|
style: {
|
|
14010
14010
|
stroke: edge_color(),
|
|
14011
14011
|
lineWidth: 1.2,
|
|
@@ -14146,11 +14146,18 @@ function build_data(gobj) {
|
|
|
14146
14146
|
let shown = pair.events.slice(0, MAX_EDGE_LABELS);
|
|
14147
14147
|
let label = shown.join("\n");
|
|
14148
14148
|
if (pair.events.length > shown.length) label += "\n+" + (pair.events.length - shown.length);
|
|
14149
|
+
let style = { labelText: label };
|
|
14150
|
+
let reciprocal = by_pair.has(pair.target + " " + pair.source);
|
|
14151
|
+
if (reciprocal) {
|
|
14152
|
+
style.curveOffset = 34;
|
|
14153
|
+
style.labelPlacement = .5;
|
|
14154
|
+
}
|
|
14149
14155
|
edges.push({
|
|
14150
14156
|
id: "e_" + edges.length,
|
|
14151
14157
|
source: pair.source,
|
|
14152
14158
|
target: pair.target,
|
|
14153
|
-
|
|
14159
|
+
data: { reciprocal },
|
|
14160
|
+
style
|
|
14154
14161
|
});
|
|
14155
14162
|
}
|
|
14156
14163
|
return {
|
|
@@ -15537,7 +15544,7 @@ function build_machine(gobj, fsm) {
|
|
|
15537
15544
|
{ class: "GCLASS_MX_ACTION" },
|
|
15538
15545
|
cell.action
|
|
15539
15546
|
]);
|
|
15540
|
-
else items.push([
|
|
15547
|
+
else if (cell.has_action) items.push([
|
|
15541
15548
|
"span",
|
|
15542
15549
|
{
|
|
15543
15550
|
class: "GCLASS_MX_ACTION is-unnamed",
|
|
@@ -15546,6 +15553,15 @@ function build_machine(gobj, fsm) {
|
|
|
15546
15553
|
},
|
|
15547
15554
|
"●"
|
|
15548
15555
|
]);
|
|
15556
|
+
else items.push([
|
|
15557
|
+
"span",
|
|
15558
|
+
{
|
|
15559
|
+
class: "GCLASS_MX_ACTION is-none-action",
|
|
15560
|
+
title: t("accepted with no action"),
|
|
15561
|
+
"data-i18n-title": "accepted with no action"
|
|
15562
|
+
},
|
|
15563
|
+
"—"
|
|
15564
|
+
]);
|
|
15549
15565
|
if (cell.next_state) {
|
|
15550
15566
|
items.push([
|
|
15551
15567
|
"span",
|
|
@@ -15674,6 +15690,19 @@ function build_legend(fsm, current) {
|
|
|
15674
15690
|
t("not declared in this state")
|
|
15675
15691
|
]]
|
|
15676
15692
|
]);
|
|
15693
|
+
if (fsm.rows.some((row) => row.cells.some((cell) => cell && !cell.has_action))) items.push([
|
|
15694
|
+
"span",
|
|
15695
|
+
{ class: "GCLASS_LEGEND_ITEM" },
|
|
15696
|
+
[[
|
|
15697
|
+
"i",
|
|
15698
|
+
{ class: "GCLASS_LEGEND_SWATCH is-none-action" },
|
|
15699
|
+
"—"
|
|
15700
|
+
], [
|
|
15701
|
+
"span",
|
|
15702
|
+
{ i18n: "accepted with no action" },
|
|
15703
|
+
t("accepted with no action")
|
|
15704
|
+
]]
|
|
15705
|
+
]);
|
|
15677
15706
|
if (current && fsm.states.some((s) => s.name === current)) items.push([
|
|
15678
15707
|
"span",
|
|
15679
15708
|
{ class: "GCLASS_LEGEND_ITEM" },
|
|
@@ -16524,6 +16553,9 @@ function open_gclass_view(host, gclass_name, opts) {
|
|
|
16524
16553
|
};
|
|
16525
16554
|
let $box = gobj_read_pointer_attr(jv, "$container");
|
|
16526
16555
|
let on_close = () => {
|
|
16556
|
+
handle.win = null;
|
|
16557
|
+
handle.modal = null;
|
|
16558
|
+
close_gclass_view(handle);
|
|
16527
16559
|
if (typeof opts.on_close === "function") opts.on_close();
|
|
16528
16560
|
};
|
|
16529
16561
|
if (is_mobile$3()) {
|
package/package.json
CHANGED
package/src/c_yui_fsm_graph.js
CHANGED
|
@@ -395,7 +395,15 @@ function build_graph(gobj)
|
|
|
395
395
|
* the edge of the box. */
|
|
396
396
|
padding: 24,
|
|
397
397
|
edge: {
|
|
398
|
-
|
|
398
|
+
/*
|
|
399
|
+
* A FUNCTION, not a name: a `type` written on the edge
|
|
400
|
+
* datum is overridden by the graph-level default, so a
|
|
401
|
+
* reciprocal pair went on drawing as two straight
|
|
402
|
+
* segments on top of each other. Asking the datum here is
|
|
403
|
+
* what G6 documents for varying the type per element.
|
|
404
|
+
*/
|
|
405
|
+
type: (d) => (d && d.data && d.data.reciprocal)?
|
|
406
|
+
'quadratic': 'polyline',
|
|
399
407
|
style: {
|
|
400
408
|
stroke: edge_color(),
|
|
401
409
|
lineWidth: 1.2,
|
|
@@ -609,11 +617,51 @@ function build_data(gobj)
|
|
|
609
617
|
if(pair.events.length > shown.length) {
|
|
610
618
|
label += "\n+" + (pair.events.length - shown.length);
|
|
611
619
|
}
|
|
620
|
+
|
|
621
|
+
let style = {labelText: label};
|
|
622
|
+
|
|
623
|
+
/*
|
|
624
|
+
* A pair that goes BOTH ways is drawn as two arcs, one bowing
|
|
625
|
+
* each way.
|
|
626
|
+
*
|
|
627
|
+
* Grouping by pair fixed the transitions that share an ordered
|
|
628
|
+
* pair; a RECIPROCAL pair is a different key and lands on the
|
|
629
|
+
* same straight segment -- so the two arrows overlap and the
|
|
630
|
+
* second label is painted under the first. Real case:
|
|
631
|
+
* C_WEBSOCKET goes to ST_WAIT_HANDSHAKE on EV_CONNECTED and
|
|
632
|
+
* comes back on EV_DISCONNECTED, and only the return was
|
|
633
|
+
* readable.
|
|
634
|
+
*/
|
|
635
|
+
let reciprocal = by_pair.has(pair.target + " " + pair.source);
|
|
636
|
+
if(reciprocal) {
|
|
637
|
+
/* Bowed opposite ways so the two arrows separate -- and
|
|
638
|
+
* the two LABELS moved off the midpoint as well, one to
|
|
639
|
+
* each third of its own curve. Bowing alone was not
|
|
640
|
+
* enough: both labels are placed at the middle by default,
|
|
641
|
+
* they carry an opaque background, and the one drawn last
|
|
642
|
+
* simply covered the other. */
|
|
643
|
+
/* The SAME offset on both, not opposite ones. G6 measures
|
|
644
|
+
* `curveOffset` from the edge's OWN direction, and a
|
|
645
|
+
* reciprocal pair runs in opposite directions -- so equal
|
|
646
|
+
* signs bow them to opposite sides of the screen, and
|
|
647
|
+
* opposite signs put them on the same side, one on top of
|
|
648
|
+
* the other.
|
|
649
|
+
*
|
|
650
|
+
* The label stays at the MIDDLE of its arc, where the two
|
|
651
|
+
* are furthest apart. Moving it along the curve instead
|
|
652
|
+
* (0.3 / 0.7) pushed it under a state card: G6's html
|
|
653
|
+
* nodes are DOM elements ABOVE the canvas, so whatever the
|
|
654
|
+
* curve draws under one is invisible. */
|
|
655
|
+
style.curveOffset = 34;
|
|
656
|
+
style.labelPlacement = 0.5;
|
|
657
|
+
}
|
|
658
|
+
|
|
612
659
|
edges.push({
|
|
613
660
|
id: "e_" + edges.length,
|
|
614
661
|
source: pair.source,
|
|
615
662
|
target: pair.target,
|
|
616
|
-
|
|
663
|
+
data: {reciprocal: reciprocal},
|
|
664
|
+
style: style,
|
|
617
665
|
});
|
|
618
666
|
}
|
|
619
667
|
|
package/src/c_yui_gclass.css
CHANGED
|
@@ -506,3 +506,19 @@
|
|
|
506
506
|
min-height: 16rem;
|
|
507
507
|
max-height: 70vh;
|
|
508
508
|
}
|
|
509
|
+
|
|
510
|
+
/*
|
|
511
|
+
* An event declared with no action at all: legal here, and it does
|
|
512
|
+
* nothing. Quieter than an action and louder than an empty cell,
|
|
513
|
+
* because it is neither.
|
|
514
|
+
*/
|
|
515
|
+
.C_YUI_GCLASS .GCLASS_MX_ACTION.is-none-action {
|
|
516
|
+
color: var(--gclass-muted);
|
|
517
|
+
cursor: help;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.C_YUI_GCLASS .GCLASS_LEGEND_SWATCH.is-none-action {
|
|
521
|
+
border-color: transparent;
|
|
522
|
+
color: var(--gclass-muted);
|
|
523
|
+
width: auto;
|
|
524
|
+
}
|
package/src/c_yui_gclass.js
CHANGED
|
@@ -910,13 +910,24 @@ function build_machine(gobj, fsm)
|
|
|
910
910
|
let items = [];
|
|
911
911
|
if(cell.action) {
|
|
912
912
|
items.push(['span', {class: 'GCLASS_MX_ACTION'}, cell.action]);
|
|
913
|
-
} else {
|
|
913
|
+
} else if(cell.has_action) {
|
|
914
914
|
/* The C side cannot name an action: `states2json()`
|
|
915
915
|
* writes the literal "action" for all of them. A mark
|
|
916
916
|
* is the honest drawing of "there is one". */
|
|
917
917
|
items.push(['span', {class: 'GCLASS_MX_ACTION is-unnamed',
|
|
918
918
|
title: t("action"),
|
|
919
919
|
'data-i18n-title': 'action'}, "●"]);
|
|
920
|
+
} else {
|
|
921
|
+
/* Declared with NO action and no next state --
|
|
922
|
+
* `{EV_TX_READY, 0, 0}`, four of them in C_WEBSOCKET.
|
|
923
|
+
* It is not the same as an empty cell (which throws)
|
|
924
|
+
* and it is not an action either: the event is legal
|
|
925
|
+
* here and deliberately does nothing. Drawing it as a
|
|
926
|
+
* mark claimed an action the gclass never declared. */
|
|
927
|
+
items.push(['span', {class: 'GCLASS_MX_ACTION is-none-action',
|
|
928
|
+
title: t("accepted with no action"),
|
|
929
|
+
'data-i18n-title': 'accepted with no action'},
|
|
930
|
+
"—"]);
|
|
920
931
|
}
|
|
921
932
|
if(cell.next_state) {
|
|
922
933
|
items.push(['span', {class: 'GCLASS_MX_ARROW'}, "▸"]);
|
|
@@ -1004,6 +1015,19 @@ function build_legend(fsm, current)
|
|
|
1004
1015
|
['span', {i18n: 'not declared in this state'}, t("not declared in this state")]
|
|
1005
1016
|
]]);
|
|
1006
1017
|
|
|
1018
|
+
/* Only if there is one on screen: a legend for a mark nobody drew
|
|
1019
|
+
* is noise, and most gclasses declare no actionless event. */
|
|
1020
|
+
let has_actionless = fsm.rows.some(
|
|
1021
|
+
(row) => row.cells.some((cell) => cell && !cell.has_action)
|
|
1022
|
+
);
|
|
1023
|
+
if(has_actionless) {
|
|
1024
|
+
items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
|
|
1025
|
+
['i', {class: 'GCLASS_LEGEND_SWATCH is-none-action'}, "—"],
|
|
1026
|
+
['span', {i18n: 'accepted with no action'},
|
|
1027
|
+
t("accepted with no action")]
|
|
1028
|
+
]]);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1007
1031
|
if(current && fsm.states.some((s) => s.name === current)) {
|
|
1008
1032
|
items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
|
|
1009
1033
|
['i', {class: 'GCLASS_LEGEND_SWATCH is-current'}],
|
|
@@ -215,6 +215,20 @@ describe("build_fsm", () => {
|
|
|
215
215
|
expect(fsm.published[0].flags).toEqual(["EVF_OUTPUT_EVENT"]);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
it("tells a declared-but-actionless cell from an action", () => {
|
|
219
|
+
/* `{EV_TX_READY, 0, 0}` -- four of them in C_WEBSOCKET: the
|
|
220
|
+
* event is legal in that state and deliberately does nothing. */
|
|
221
|
+
const none = build_fsm({
|
|
222
|
+
events: [{event_name: "EV_TX_READY", event_flag: ""}],
|
|
223
|
+
states: {"ST_IDLE": [
|
|
224
|
+
{event_name: "EV_TX_READY", action: "", next_state: ""},
|
|
225
|
+
]},
|
|
226
|
+
});
|
|
227
|
+
expect(none.rows[0].cells[0]).toEqual({
|
|
228
|
+
action: "", has_action: false, next_state: "",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
218
232
|
it("says an action exists without inventing its name", () => {
|
|
219
233
|
expect(fsm.rows[0].cells[1]).toEqual({
|
|
220
234
|
action: "", has_action: true, next_state: "",
|
package/src/yui_gclass_view.js
CHANGED
|
@@ -158,7 +158,30 @@ function open_gclass_view(host, gclass_name, opts)
|
|
|
158
158
|
let handle = {json_gobj: jv, win: null, modal: null};
|
|
159
159
|
let $box = gobj_read_pointer_attr(jv, "$container");
|
|
160
160
|
|
|
161
|
+
/*
|
|
162
|
+
* The ✕ path, and the ONLY one that used to leave the viewer
|
|
163
|
+
* behind. `close_window()` calls this and THEN destroys the
|
|
164
|
+
* window -- but the viewer is not the window's child (it hangs
|
|
165
|
+
* from the host, so it can outlive a window that is only its
|
|
166
|
+
* frame), so nothing else took it down. Alive, it did two things:
|
|
167
|
+
*
|
|
168
|
+
* - it kept its service NAME, so the very next click on the
|
|
169
|
+
* `gclass` button answered "service ALREADY registered:
|
|
170
|
+
* C_YUI_GCLASS^gclass-view-<host>" and no second gclass could
|
|
171
|
+
* be opened for the rest of the session.
|
|
172
|
+
* - it was still RUNNING when the host's own window closed, and
|
|
173
|
+
* the framework logged "Destroying a RUNNING gobj" while
|
|
174
|
+
* taking it down with its parent.
|
|
175
|
+
*
|
|
176
|
+
* The window and the sheet are cleared FIRST: both are already
|
|
177
|
+
* tearing themselves down, and close_gclass_view() must not go
|
|
178
|
+
* back at them.
|
|
179
|
+
*/
|
|
161
180
|
let on_close = () => {
|
|
181
|
+
handle.win = null;
|
|
182
|
+
handle.modal = null;
|
|
183
|
+
close_gclass_view(handle);
|
|
184
|
+
|
|
162
185
|
if(typeof opts.on_close === "function") {
|
|
163
186
|
opts.on_close();
|
|
164
187
|
}
|