@yuneta/gobj-ui 7.23.71 → 7.23.73
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 +29 -0
- package/dist/gobj-ui.cjs.js +106 -74
- package/dist/gobj-ui.es.js +106 -74
- package/package.json +1 -1
- package/src/c_yui_treedb_graph.js +16 -6
- package/src/c_yui_treedb_topic_with_form.js +2 -0
- package/src/priv_declared.test.js +212 -0
- package/src/treedb_layout.js +95 -60
- package/src/treedb_layout.test.js +32 -0
package/README.md
CHANGED
|
@@ -1882,6 +1882,35 @@ the middle, and **a toolbar left with a single group is centred**. An unknown
|
|
|
1882
1882
|
name is reported, not silently dropped: a typo would otherwise remove the save
|
|
1883
1883
|
button with no trace of why.
|
|
1884
1884
|
|
|
1885
|
+
### `C_YUI_FORM` — which field is editable, and the one `writable` does not govern
|
|
1886
|
+
|
|
1887
|
+
A form opened to LOOK at a record has no editable field: that is a property of
|
|
1888
|
+
the opening (`readonly`), not of the schema, and it wins over everything. Under
|
|
1889
|
+
it the schema decides — a column needs `writable` — **with one exception, and
|
|
1890
|
+
it is the whole of linking: an fkey.**
|
|
1891
|
+
|
|
1892
|
+
`writable` governs the WRITE of a column's VALUE, and an fkey is not written,
|
|
1893
|
+
it is **linked**. An fkey is normally declared with no `writable` flag at all
|
|
1894
|
+
(`treedb_authzs`'s `users.roles` is `['fkey']`, and so is almost every fkey in
|
|
1895
|
+
the tree), so reading "not writable" as "not editable" takes away the only way
|
|
1896
|
+
to link a record to its parent — with `users.roles`, the only way to give a
|
|
1897
|
+
person a role. The other half of the same rule is in the topic view, where the
|
|
1898
|
+
record travels back: it sends "the writable cols, the fkeys (a link is edited
|
|
1899
|
+
by linking) and the pkey". The two halves have to agree; when they did not, the
|
|
1900
|
+
form disabled the control whose value the save was still waiting for.
|
|
1901
|
+
|
|
1902
|
+
The rule lives in `form_field_readonly.js`, pure and tested. A `file` column is
|
|
1903
|
+
an fkey too (`['fkey','file']`) and is deliberately NOT covered: it answers
|
|
1904
|
+
`type: "file"`, its bytes travel beside the record, and the SDK declares as
|
|
1905
|
+
legal a `file` column that only a load fills.
|
|
1906
|
+
|
|
1907
|
+
**And `readonly` is an attribute of a TEXT control and of nothing else.** A
|
|
1908
|
+
`<select>`, a checkbox and a radio accept it and let the reader change the
|
|
1909
|
+
value anyway; only `disabled` stops them, and tom-select has to be told through
|
|
1910
|
+
its API. So the form applies `disabled` to those three, and the record is still
|
|
1911
|
+
read from a disabled control — it is collected by reading each marked control,
|
|
1912
|
+
never by submitting the form.
|
|
1913
|
+
|
|
1885
1914
|
### Selecting rows in any table — `yui_table_select.js`
|
|
1886
1915
|
|
|
1887
1916
|
Deleting twenty rows one confirmation at a time is not a workflow. Any view
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -25970,6 +25970,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
25970
25970
|
* fkeys, times and numbers). Route by the form's mode.
|
|
25971
25971
|
************************************************************/
|
|
25972
25972
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
25973
|
+
let priv = gobj.priv;
|
|
25973
25974
|
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
25974
25975
|
let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode");
|
|
25975
25976
|
if (priv.reading_files) {
|
|
@@ -27491,6 +27492,28 @@ function spanning_tree(nodes, edges) {
|
|
|
27491
27492
|
};
|
|
27492
27493
|
}
|
|
27493
27494
|
/************************************************************
|
|
27495
|
+
* The ids of a spanning tree in PARENT-BEFORE-CHILD order.
|
|
27496
|
+
*
|
|
27497
|
+
* Every pass below used to be a recursive walk, which is the natural
|
|
27498
|
+
* way to write it and the one that dies on a deep tree: a
|
|
27499
|
+
* self-referent hook (a place inside a place inside a place) is as
|
|
27500
|
+
* deep as the data says, and the stack is not. Read this list
|
|
27501
|
+
* forwards for a pre-order pass and BACKWARDS for a post-order one
|
|
27502
|
+
* -- a child always sits after its parent, so reversed it always
|
|
27503
|
+
* sits before it, which is the whole of what post-order was for.
|
|
27504
|
+
************************************************************/
|
|
27505
|
+
function walk_order(t) {
|
|
27506
|
+
let order = [];
|
|
27507
|
+
let stack = t.roots.slice().reverse();
|
|
27508
|
+
while (stack.length) {
|
|
27509
|
+
let id = stack.pop();
|
|
27510
|
+
order.push(id);
|
|
27511
|
+
let kids = t.children.get(id) || [];
|
|
27512
|
+
for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
|
|
27513
|
+
}
|
|
27514
|
+
return order;
|
|
27515
|
+
}
|
|
27516
|
+
/************************************************************
|
|
27494
27517
|
* The tidy tree, left to right.
|
|
27495
27518
|
*
|
|
27496
27519
|
* Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
|
|
@@ -27511,56 +27534,45 @@ function layout_tree(nodes, edges, opts) {
|
|
|
27511
27534
|
col_x[d] = x;
|
|
27512
27535
|
x += (col_w[d] || 0) + o.ranksep;
|
|
27513
27536
|
}
|
|
27537
|
+
let order = walk_order(t);
|
|
27514
27538
|
let height = /* @__PURE__ */ new Map();
|
|
27515
|
-
let
|
|
27539
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
27540
|
+
let id = order[i];
|
|
27516
27541
|
let kids = t.children.get(id) || [];
|
|
27517
27542
|
let own = t.by_id.get(id).h;
|
|
27518
|
-
if (!kids.length) {
|
|
27519
|
-
height.set(id, own);
|
|
27520
|
-
return own;
|
|
27521
|
-
}
|
|
27522
27543
|
let sum = 0;
|
|
27523
|
-
for (let
|
|
27524
|
-
sum +=
|
|
27525
|
-
if (
|
|
27544
|
+
for (let k = 0; k < kids.length; k++) {
|
|
27545
|
+
sum += height.get(kids[k]);
|
|
27546
|
+
if (k > 0) sum += o.nodesep;
|
|
27526
27547
|
}
|
|
27527
|
-
|
|
27528
|
-
|
|
27529
|
-
|
|
27530
|
-
|
|
27531
|
-
for (let root of t.roots)
|
|
27532
|
-
|
|
27533
|
-
|
|
27548
|
+
height.set(id, kids.length ? Math.max(own, sum) : own);
|
|
27549
|
+
}
|
|
27550
|
+
let tops = /* @__PURE__ */ new Map();
|
|
27551
|
+
let top = 0;
|
|
27552
|
+
for (let root of t.roots) {
|
|
27553
|
+
tops.set(root, top);
|
|
27554
|
+
top += height.get(root) + o.nodesep * 2;
|
|
27555
|
+
}
|
|
27556
|
+
for (let id of order) {
|
|
27534
27557
|
let d = t.depth.get(id);
|
|
27535
27558
|
let kids = t.children.get(id) || [];
|
|
27536
27559
|
let block = height.get(id);
|
|
27537
|
-
let
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
return;
|
|
27544
|
-
}
|
|
27560
|
+
let own_top = tops.get(id);
|
|
27561
|
+
pos.set(id, {
|
|
27562
|
+
x: col_x[d] + col_w[d] / 2,
|
|
27563
|
+
y: own_top + block / 2
|
|
27564
|
+
});
|
|
27565
|
+
if (!kids.length) continue;
|
|
27545
27566
|
let kids_h = 0;
|
|
27546
|
-
for (let
|
|
27547
|
-
kids_h += height.get(kids[
|
|
27548
|
-
if (
|
|
27567
|
+
for (let k = 0; k < kids.length; k++) {
|
|
27568
|
+
kids_h += height.get(kids[k]);
|
|
27569
|
+
if (k > 0) kids_h += o.nodesep;
|
|
27549
27570
|
}
|
|
27550
|
-
let cursor =
|
|
27571
|
+
let cursor = own_top + (block - kids_h) / 2;
|
|
27551
27572
|
for (let kid of kids) {
|
|
27552
|
-
|
|
27573
|
+
tops.set(kid, cursor);
|
|
27553
27574
|
cursor += height.get(kid) + o.nodesep;
|
|
27554
27575
|
}
|
|
27555
|
-
pos.set(id, {
|
|
27556
|
-
x: cx,
|
|
27557
|
-
y: top + block / 2
|
|
27558
|
-
});
|
|
27559
|
-
};
|
|
27560
|
-
let top = 0;
|
|
27561
|
-
for (let root of t.roots) {
|
|
27562
|
-
place(root, top);
|
|
27563
|
-
top += height.get(root) + o.nodesep * 2;
|
|
27564
27576
|
}
|
|
27565
27577
|
return pos;
|
|
27566
27578
|
}
|
|
@@ -27587,40 +27599,53 @@ function layout_radial(nodes, edges, opts) {
|
|
|
27587
27599
|
let t = spanning_tree(nodes, edges);
|
|
27588
27600
|
let pos = /* @__PURE__ */ new Map();
|
|
27589
27601
|
if (!t.roots.length) return pos;
|
|
27602
|
+
let order = walk_order(t);
|
|
27590
27603
|
let leaves = /* @__PURE__ */ new Map();
|
|
27591
|
-
let
|
|
27604
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
27605
|
+
let id = order[i];
|
|
27592
27606
|
let kids = t.children.get(id) || [];
|
|
27593
27607
|
if (!kids.length) {
|
|
27594
27608
|
leaves.set(id, 1);
|
|
27595
|
-
|
|
27609
|
+
continue;
|
|
27596
27610
|
}
|
|
27597
27611
|
let n = 0;
|
|
27598
|
-
for (let kid of kids) n +=
|
|
27612
|
+
for (let kid of kids) n += leaves.get(kid);
|
|
27599
27613
|
leaves.set(id, n);
|
|
27600
|
-
|
|
27601
|
-
};
|
|
27614
|
+
}
|
|
27602
27615
|
let total = 0;
|
|
27603
|
-
for (let root of t.roots) total +=
|
|
27616
|
+
for (let root of t.roots) total += leaves.get(root);
|
|
27604
27617
|
let single = t.roots.length === 1;
|
|
27605
27618
|
let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
|
|
27606
27619
|
let angle = /* @__PURE__ */ new Map();
|
|
27607
27620
|
let rings = [];
|
|
27608
|
-
let sector = (
|
|
27609
|
-
let
|
|
27610
|
-
|
|
27611
|
-
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
id,
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
27620
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
|
|
27621
|
+
let sector = (root_id, root_from, root_span) => {
|
|
27622
|
+
let stack = [{
|
|
27623
|
+
id: root_id,
|
|
27624
|
+
from: root_from,
|
|
27625
|
+
span: root_span
|
|
27626
|
+
}];
|
|
27627
|
+
while (stack.length) {
|
|
27628
|
+
let { id, from, span } = stack.pop();
|
|
27629
|
+
let a = from + span / 2;
|
|
27630
|
+
angle.set(id, a);
|
|
27631
|
+
let ring = ring_of(id);
|
|
27632
|
+
if (!rings[ring]) rings[ring] = [];
|
|
27633
|
+
let n = t.by_id.get(id);
|
|
27634
|
+
rings[ring].push({
|
|
27635
|
+
id,
|
|
27636
|
+
angle: a,
|
|
27637
|
+
diag: Math.hypot(n.w, n.h)
|
|
27638
|
+
});
|
|
27639
|
+
let cursor = from;
|
|
27640
|
+
for (let kid of t.children.get(id) || []) {
|
|
27641
|
+
let part = span * leaves.get(kid) / leaves.get(id);
|
|
27642
|
+
stack.push({
|
|
27643
|
+
id: kid,
|
|
27644
|
+
from: cursor,
|
|
27645
|
+
span: part
|
|
27646
|
+
});
|
|
27647
|
+
cursor += part;
|
|
27648
|
+
}
|
|
27624
27649
|
}
|
|
27625
27650
|
};
|
|
27626
27651
|
if (single) {
|
|
@@ -27671,15 +27696,20 @@ function layout_outline(nodes, edges, opts) {
|
|
|
27671
27696
|
let t = spanning_tree(nodes, edges);
|
|
27672
27697
|
let pos = /* @__PURE__ */ new Map();
|
|
27673
27698
|
let y = 0;
|
|
27674
|
-
let visit = (
|
|
27675
|
-
let
|
|
27676
|
-
|
|
27677
|
-
|
|
27678
|
-
|
|
27679
|
-
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27699
|
+
let visit = (root_id) => {
|
|
27700
|
+
let stack = [root_id];
|
|
27701
|
+
while (stack.length) {
|
|
27702
|
+
let id = stack.pop();
|
|
27703
|
+
let n = t.by_id.get(id);
|
|
27704
|
+
let d = t.depth.get(id);
|
|
27705
|
+
pos.set(id, {
|
|
27706
|
+
x: d * o.indent + n.w / 2,
|
|
27707
|
+
y: y + n.h / 2
|
|
27708
|
+
});
|
|
27709
|
+
y += n.h + o.nodesep;
|
|
27710
|
+
let kids = t.children.get(id) || [];
|
|
27711
|
+
for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
|
|
27712
|
+
}
|
|
27683
27713
|
};
|
|
27684
27714
|
for (let root of t.roots) {
|
|
27685
27715
|
visit(root);
|
|
@@ -62106,6 +62136,8 @@ function ac_set_operation_mode(gobj, event, kw, src) {
|
|
|
62106
62136
|
* into the URL, so what you are looking at stays linkable.
|
|
62107
62137
|
************************************************************/
|
|
62108
62138
|
var LEGEND_GLYPH_STYLE = "font-size:1.5em; line-height:1; color:inherit;";
|
|
62139
|
+
var LEGEND_NAME_STYLE = "font-size:1.2em; line-height:1.2;";
|
|
62140
|
+
var LEGEND_COUNT_STYLE = "margin-left:.4rem; font-size:1em;";
|
|
62109
62141
|
function refresh_legend(gobj) {
|
|
62110
62142
|
let priv = gobj.priv;
|
|
62111
62143
|
let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
|
|
@@ -62131,19 +62163,19 @@ function refresh_legend(gobj) {
|
|
|
62131
62163
|
"span",
|
|
62132
62164
|
{
|
|
62133
62165
|
class: "GRAPH_LEGEND_NAME",
|
|
62134
|
-
style: hidden ? "text-decoration:line-through; opacity:.6;" : ""
|
|
62166
|
+
style: LEGEND_NAME_STYLE + (hidden ? " text-decoration:line-through; opacity:.6;" : "")
|
|
62135
62167
|
},
|
|
62136
62168
|
topic_name
|
|
62137
62169
|
], [
|
|
62138
62170
|
"span",
|
|
62139
62171
|
{
|
|
62140
62172
|
class: "GRAPH_LEGEND_COUNT has-text-grey",
|
|
62141
|
-
style:
|
|
62173
|
+
style: LEGEND_COUNT_STYLE
|
|
62142
62174
|
},
|
|
62143
62175
|
hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
|
|
62144
62176
|
]);
|
|
62145
62177
|
let body_attrs = {
|
|
62146
|
-
class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? "
|
|
62178
|
+
class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " pressed_state" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
|
|
62147
62179
|
type: "button",
|
|
62148
62180
|
style: "gap:0; flex:0 0 auto;",
|
|
62149
62181
|
title: is_main ? (0, i18next.t)("main topic") : hidden ? (0, i18next.t)("show topic") : (0, i18next.t)("hide topic"),
|
|
@@ -62167,7 +62199,7 @@ function refresh_legend(gobj) {
|
|
|
62167
62199
|
if (is_main && state.main_chosen) extras.push([
|
|
62168
62200
|
"button",
|
|
62169
62201
|
{
|
|
62170
|
-
class: "GRAPH_LEGEND_STAR button is-small
|
|
62202
|
+
class: "GRAPH_LEGEND_STAR button is-small pressed_state",
|
|
62171
62203
|
type: "button",
|
|
62172
62204
|
style: "padding:0 .4rem;",
|
|
62173
62205
|
title: (0, i18next.t)("main topic"),
|
|
@@ -62228,7 +62260,7 @@ function refresh_legend(gobj) {
|
|
|
62228
62260
|
if (!hidden && entry.linked && entry.loose > 0) extras.push([
|
|
62229
62261
|
"button",
|
|
62230
62262
|
{
|
|
62231
|
-
class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? "
|
|
62263
|
+
class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " pressed_state" : ""),
|
|
62232
62264
|
type: "button",
|
|
62233
62265
|
style: "padding:0 .45rem;",
|
|
62234
62266
|
title: (0, i18next.t)("loose records"),
|
|
@@ -62248,7 +62280,7 @@ function refresh_legend(gobj) {
|
|
|
62248
62280
|
if (!hidden) extras.push([
|
|
62249
62281
|
"button",
|
|
62250
62282
|
{
|
|
62251
|
-
class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? "
|
|
62283
|
+
class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " pressed_state" : ""),
|
|
62252
62284
|
type: "button",
|
|
62253
62285
|
style: "padding:0 .4rem;",
|
|
62254
62286
|
title: (0, i18next.t)("highlight topic"),
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -25965,6 +25965,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
25965
25965
|
* fkeys, times and numbers). Route by the form's mode.
|
|
25966
25966
|
************************************************************/
|
|
25967
25967
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
25968
|
+
let priv = gobj.priv;
|
|
25968
25969
|
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
25969
25970
|
let mode = gobj_read_str_attr(src, "form_mode");
|
|
25970
25971
|
if (priv.reading_files) {
|
|
@@ -27486,6 +27487,28 @@ function spanning_tree(nodes, edges) {
|
|
|
27486
27487
|
};
|
|
27487
27488
|
}
|
|
27488
27489
|
/************************************************************
|
|
27490
|
+
* The ids of a spanning tree in PARENT-BEFORE-CHILD order.
|
|
27491
|
+
*
|
|
27492
|
+
* Every pass below used to be a recursive walk, which is the natural
|
|
27493
|
+
* way to write it and the one that dies on a deep tree: a
|
|
27494
|
+
* self-referent hook (a place inside a place inside a place) is as
|
|
27495
|
+
* deep as the data says, and the stack is not. Read this list
|
|
27496
|
+
* forwards for a pre-order pass and BACKWARDS for a post-order one
|
|
27497
|
+
* -- a child always sits after its parent, so reversed it always
|
|
27498
|
+
* sits before it, which is the whole of what post-order was for.
|
|
27499
|
+
************************************************************/
|
|
27500
|
+
function walk_order(t) {
|
|
27501
|
+
let order = [];
|
|
27502
|
+
let stack = t.roots.slice().reverse();
|
|
27503
|
+
while (stack.length) {
|
|
27504
|
+
let id = stack.pop();
|
|
27505
|
+
order.push(id);
|
|
27506
|
+
let kids = t.children.get(id) || [];
|
|
27507
|
+
for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
|
|
27508
|
+
}
|
|
27509
|
+
return order;
|
|
27510
|
+
}
|
|
27511
|
+
/************************************************************
|
|
27489
27512
|
* The tidy tree, left to right.
|
|
27490
27513
|
*
|
|
27491
27514
|
* Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
|
|
@@ -27506,56 +27529,45 @@ function layout_tree(nodes, edges, opts) {
|
|
|
27506
27529
|
col_x[d] = x;
|
|
27507
27530
|
x += (col_w[d] || 0) + o.ranksep;
|
|
27508
27531
|
}
|
|
27532
|
+
let order = walk_order(t);
|
|
27509
27533
|
let height = /* @__PURE__ */ new Map();
|
|
27510
|
-
let
|
|
27534
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
27535
|
+
let id = order[i];
|
|
27511
27536
|
let kids = t.children.get(id) || [];
|
|
27512
27537
|
let own = t.by_id.get(id).h;
|
|
27513
|
-
if (!kids.length) {
|
|
27514
|
-
height.set(id, own);
|
|
27515
|
-
return own;
|
|
27516
|
-
}
|
|
27517
27538
|
let sum = 0;
|
|
27518
|
-
for (let
|
|
27519
|
-
sum +=
|
|
27520
|
-
if (
|
|
27539
|
+
for (let k = 0; k < kids.length; k++) {
|
|
27540
|
+
sum += height.get(kids[k]);
|
|
27541
|
+
if (k > 0) sum += o.nodesep;
|
|
27521
27542
|
}
|
|
27522
|
-
|
|
27523
|
-
|
|
27524
|
-
|
|
27525
|
-
|
|
27526
|
-
for (let root of t.roots)
|
|
27527
|
-
|
|
27528
|
-
|
|
27543
|
+
height.set(id, kids.length ? Math.max(own, sum) : own);
|
|
27544
|
+
}
|
|
27545
|
+
let tops = /* @__PURE__ */ new Map();
|
|
27546
|
+
let top = 0;
|
|
27547
|
+
for (let root of t.roots) {
|
|
27548
|
+
tops.set(root, top);
|
|
27549
|
+
top += height.get(root) + o.nodesep * 2;
|
|
27550
|
+
}
|
|
27551
|
+
for (let id of order) {
|
|
27529
27552
|
let d = t.depth.get(id);
|
|
27530
27553
|
let kids = t.children.get(id) || [];
|
|
27531
27554
|
let block = height.get(id);
|
|
27532
|
-
let
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
return;
|
|
27539
|
-
}
|
|
27555
|
+
let own_top = tops.get(id);
|
|
27556
|
+
pos.set(id, {
|
|
27557
|
+
x: col_x[d] + col_w[d] / 2,
|
|
27558
|
+
y: own_top + block / 2
|
|
27559
|
+
});
|
|
27560
|
+
if (!kids.length) continue;
|
|
27540
27561
|
let kids_h = 0;
|
|
27541
|
-
for (let
|
|
27542
|
-
kids_h += height.get(kids[
|
|
27543
|
-
if (
|
|
27562
|
+
for (let k = 0; k < kids.length; k++) {
|
|
27563
|
+
kids_h += height.get(kids[k]);
|
|
27564
|
+
if (k > 0) kids_h += o.nodesep;
|
|
27544
27565
|
}
|
|
27545
|
-
let cursor =
|
|
27566
|
+
let cursor = own_top + (block - kids_h) / 2;
|
|
27546
27567
|
for (let kid of kids) {
|
|
27547
|
-
|
|
27568
|
+
tops.set(kid, cursor);
|
|
27548
27569
|
cursor += height.get(kid) + o.nodesep;
|
|
27549
27570
|
}
|
|
27550
|
-
pos.set(id, {
|
|
27551
|
-
x: cx,
|
|
27552
|
-
y: top + block / 2
|
|
27553
|
-
});
|
|
27554
|
-
};
|
|
27555
|
-
let top = 0;
|
|
27556
|
-
for (let root of t.roots) {
|
|
27557
|
-
place(root, top);
|
|
27558
|
-
top += height.get(root) + o.nodesep * 2;
|
|
27559
27571
|
}
|
|
27560
27572
|
return pos;
|
|
27561
27573
|
}
|
|
@@ -27582,40 +27594,53 @@ function layout_radial(nodes, edges, opts) {
|
|
|
27582
27594
|
let t = spanning_tree(nodes, edges);
|
|
27583
27595
|
let pos = /* @__PURE__ */ new Map();
|
|
27584
27596
|
if (!t.roots.length) return pos;
|
|
27597
|
+
let order = walk_order(t);
|
|
27585
27598
|
let leaves = /* @__PURE__ */ new Map();
|
|
27586
|
-
let
|
|
27599
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
27600
|
+
let id = order[i];
|
|
27587
27601
|
let kids = t.children.get(id) || [];
|
|
27588
27602
|
if (!kids.length) {
|
|
27589
27603
|
leaves.set(id, 1);
|
|
27590
|
-
|
|
27604
|
+
continue;
|
|
27591
27605
|
}
|
|
27592
27606
|
let n = 0;
|
|
27593
|
-
for (let kid of kids) n +=
|
|
27607
|
+
for (let kid of kids) n += leaves.get(kid);
|
|
27594
27608
|
leaves.set(id, n);
|
|
27595
|
-
|
|
27596
|
-
};
|
|
27609
|
+
}
|
|
27597
27610
|
let total = 0;
|
|
27598
|
-
for (let root of t.roots) total +=
|
|
27611
|
+
for (let root of t.roots) total += leaves.get(root);
|
|
27599
27612
|
let single = t.roots.length === 1;
|
|
27600
27613
|
let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
|
|
27601
27614
|
let angle = /* @__PURE__ */ new Map();
|
|
27602
27615
|
let rings = [];
|
|
27603
|
-
let sector = (
|
|
27604
|
-
let
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
id,
|
|
27611
|
-
|
|
27612
|
-
|
|
27613
|
-
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27616
|
+
let sector = (root_id, root_from, root_span) => {
|
|
27617
|
+
let stack = [{
|
|
27618
|
+
id: root_id,
|
|
27619
|
+
from: root_from,
|
|
27620
|
+
span: root_span
|
|
27621
|
+
}];
|
|
27622
|
+
while (stack.length) {
|
|
27623
|
+
let { id, from, span } = stack.pop();
|
|
27624
|
+
let a = from + span / 2;
|
|
27625
|
+
angle.set(id, a);
|
|
27626
|
+
let ring = ring_of(id);
|
|
27627
|
+
if (!rings[ring]) rings[ring] = [];
|
|
27628
|
+
let n = t.by_id.get(id);
|
|
27629
|
+
rings[ring].push({
|
|
27630
|
+
id,
|
|
27631
|
+
angle: a,
|
|
27632
|
+
diag: Math.hypot(n.w, n.h)
|
|
27633
|
+
});
|
|
27634
|
+
let cursor = from;
|
|
27635
|
+
for (let kid of t.children.get(id) || []) {
|
|
27636
|
+
let part = span * leaves.get(kid) / leaves.get(id);
|
|
27637
|
+
stack.push({
|
|
27638
|
+
id: kid,
|
|
27639
|
+
from: cursor,
|
|
27640
|
+
span: part
|
|
27641
|
+
});
|
|
27642
|
+
cursor += part;
|
|
27643
|
+
}
|
|
27619
27644
|
}
|
|
27620
27645
|
};
|
|
27621
27646
|
if (single) {
|
|
@@ -27666,15 +27691,20 @@ function layout_outline(nodes, edges, opts) {
|
|
|
27666
27691
|
let t = spanning_tree(nodes, edges);
|
|
27667
27692
|
let pos = /* @__PURE__ */ new Map();
|
|
27668
27693
|
let y = 0;
|
|
27669
|
-
let visit = (
|
|
27670
|
-
let
|
|
27671
|
-
|
|
27672
|
-
|
|
27673
|
-
|
|
27674
|
-
|
|
27675
|
-
|
|
27676
|
-
|
|
27677
|
-
|
|
27694
|
+
let visit = (root_id) => {
|
|
27695
|
+
let stack = [root_id];
|
|
27696
|
+
while (stack.length) {
|
|
27697
|
+
let id = stack.pop();
|
|
27698
|
+
let n = t.by_id.get(id);
|
|
27699
|
+
let d = t.depth.get(id);
|
|
27700
|
+
pos.set(id, {
|
|
27701
|
+
x: d * o.indent + n.w / 2,
|
|
27702
|
+
y: y + n.h / 2
|
|
27703
|
+
});
|
|
27704
|
+
y += n.h + o.nodesep;
|
|
27705
|
+
let kids = t.children.get(id) || [];
|
|
27706
|
+
for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
|
|
27707
|
+
}
|
|
27678
27708
|
};
|
|
27679
27709
|
for (let root of t.roots) {
|
|
27680
27710
|
visit(root);
|
|
@@ -62101,6 +62131,8 @@ function ac_set_operation_mode(gobj, event, kw, src) {
|
|
|
62101
62131
|
* into the URL, so what you are looking at stays linkable.
|
|
62102
62132
|
************************************************************/
|
|
62103
62133
|
var LEGEND_GLYPH_STYLE = "font-size:1.5em; line-height:1; color:inherit;";
|
|
62134
|
+
var LEGEND_NAME_STYLE = "font-size:1.2em; line-height:1.2;";
|
|
62135
|
+
var LEGEND_COUNT_STYLE = "margin-left:.4rem; font-size:1em;";
|
|
62104
62136
|
function refresh_legend(gobj) {
|
|
62105
62137
|
let priv = gobj.priv;
|
|
62106
62138
|
let $container = gobj_read_attr(gobj, "$container");
|
|
@@ -62126,19 +62158,19 @@ function refresh_legend(gobj) {
|
|
|
62126
62158
|
"span",
|
|
62127
62159
|
{
|
|
62128
62160
|
class: "GRAPH_LEGEND_NAME",
|
|
62129
|
-
style: hidden ? "text-decoration:line-through; opacity:.6;" : ""
|
|
62161
|
+
style: LEGEND_NAME_STYLE + (hidden ? " text-decoration:line-through; opacity:.6;" : "")
|
|
62130
62162
|
},
|
|
62131
62163
|
topic_name
|
|
62132
62164
|
], [
|
|
62133
62165
|
"span",
|
|
62134
62166
|
{
|
|
62135
62167
|
class: "GRAPH_LEGEND_COUNT has-text-grey",
|
|
62136
|
-
style:
|
|
62168
|
+
style: LEGEND_COUNT_STYLE
|
|
62137
62169
|
},
|
|
62138
62170
|
hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
|
|
62139
62171
|
]);
|
|
62140
62172
|
let body_attrs = {
|
|
62141
|
-
class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? "
|
|
62173
|
+
class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " pressed_state" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
|
|
62142
62174
|
type: "button",
|
|
62143
62175
|
style: "gap:0; flex:0 0 auto;",
|
|
62144
62176
|
title: is_main ? t("main topic") : hidden ? t("show topic") : t("hide topic"),
|
|
@@ -62162,7 +62194,7 @@ function refresh_legend(gobj) {
|
|
|
62162
62194
|
if (is_main && state.main_chosen) extras.push([
|
|
62163
62195
|
"button",
|
|
62164
62196
|
{
|
|
62165
|
-
class: "GRAPH_LEGEND_STAR button is-small
|
|
62197
|
+
class: "GRAPH_LEGEND_STAR button is-small pressed_state",
|
|
62166
62198
|
type: "button",
|
|
62167
62199
|
style: "padding:0 .4rem;",
|
|
62168
62200
|
title: t("main topic"),
|
|
@@ -62223,7 +62255,7 @@ function refresh_legend(gobj) {
|
|
|
62223
62255
|
if (!hidden && entry.linked && entry.loose > 0) extras.push([
|
|
62224
62256
|
"button",
|
|
62225
62257
|
{
|
|
62226
|
-
class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? "
|
|
62258
|
+
class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " pressed_state" : ""),
|
|
62227
62259
|
type: "button",
|
|
62228
62260
|
style: "padding:0 .45rem;",
|
|
62229
62261
|
title: t("loose records"),
|
|
@@ -62243,7 +62275,7 @@ function refresh_legend(gobj) {
|
|
|
62243
62275
|
if (!hidden) extras.push([
|
|
62244
62276
|
"button",
|
|
62245
62277
|
{
|
|
62246
|
-
class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? "
|
|
62278
|
+
class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " pressed_state" : ""),
|
|
62247
62279
|
type: "button",
|
|
62248
62280
|
style: "padding:0 .4rem;",
|
|
62249
62281
|
title: t("highlight topic"),
|
package/package.json
CHANGED
|
@@ -2339,6 +2339,15 @@ function ac_set_operation_mode(gobj, event, kw, src)
|
|
|
2339
2339
|
* stays small so the strip keeps its height; the glyph inside grows. */
|
|
2340
2340
|
const LEGEND_GLYPH_STYLE = 'font-size:1.5em; line-height:1; color:inherit;';
|
|
2341
2341
|
|
|
2342
|
+
/* And the same for what a chip SAYS. The legend is a layer control --
|
|
2343
|
+
* a gadget somebody uses, not a footnote somebody glances at -- and a
|
|
2344
|
+
* topic name is the thing it is read for, so 0.75rem is the wrong
|
|
2345
|
+
* size for it. The button stays `is-small` because the strip has to
|
|
2346
|
+
* hold a chip per topic on one row; the name and the count come back
|
|
2347
|
+
* up inside it, exactly as the glyphs did. */
|
|
2348
|
+
const LEGEND_NAME_STYLE = 'font-size:1.2em; line-height:1.2;';
|
|
2349
|
+
const LEGEND_COUNT_STYLE = 'margin-left:.4rem; font-size:1em;';
|
|
2350
|
+
|
|
2342
2351
|
function refresh_legend(gobj)
|
|
2343
2352
|
{
|
|
2344
2353
|
let priv = gobj.priv;
|
|
@@ -2389,10 +2398,11 @@ function refresh_legend(gobj)
|
|
|
2389
2398
|
`border:1px solid rgba(0,0,0,.25);` +
|
|
2390
2399
|
(hidden? 'opacity:.35;' : '')}],
|
|
2391
2400
|
['span', {class: 'GRAPH_LEGEND_NAME',
|
|
2392
|
-
style:
|
|
2401
|
+
style: LEGEND_NAME_STYLE +
|
|
2402
|
+
(hidden? ' text-decoration:line-through; opacity:.6;' : '')},
|
|
2393
2403
|
topic_name],
|
|
2394
2404
|
['span', {class: 'GRAPH_LEGEND_COUNT has-text-grey',
|
|
2395
|
-
style:
|
|
2405
|
+
style: LEGEND_COUNT_STYLE},
|
|
2396
2406
|
hidden? `${entry.total}` : `${entry.visible}/${entry.total}`]
|
|
2397
2407
|
);
|
|
2398
2408
|
|
|
@@ -2402,7 +2412,7 @@ function refresh_legend(gobj)
|
|
|
2402
2412
|
* every chip was dead except its star. */
|
|
2403
2413
|
let body_attrs = {
|
|
2404
2414
|
class: 'GRAPH_LEGEND_ITEM button is-small' +
|
|
2405
|
-
(focused? '
|
|
2415
|
+
(focused? ' pressed_state' : '') +
|
|
2406
2416
|
(is_main? ' GRAPH_LEGEND_MAIN' : ''),
|
|
2407
2417
|
type: 'button',
|
|
2408
2418
|
style: 'gap:0; flex:0 0 auto;',
|
|
@@ -2430,7 +2440,7 @@ function refresh_legend(gobj)
|
|
|
2430
2440
|
* is a button too: pressing it hands the choice back to the
|
|
2431
2441
|
* graph (deduced again). On a deduced one it is a mark. */
|
|
2432
2442
|
if(is_main && state.main_chosen) {
|
|
2433
|
-
extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small
|
|
2443
|
+
extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small pressed_state',
|
|
2434
2444
|
type: 'button', style: 'padding:0 .4rem;',
|
|
2435
2445
|
title: t('main topic'), 'data-i18n-title': 'main topic',
|
|
2436
2446
|
'aria-label': t('main topic'), 'aria-pressed': 'true'},
|
|
@@ -2466,7 +2476,7 @@ function refresh_legend(gobj)
|
|
|
2466
2476
|
if(!hidden && entry.linked && entry.loose > 0) {
|
|
2467
2477
|
extras.push(['button', {
|
|
2468
2478
|
class: 'GRAPH_LEGEND_LOOSE button is-small' +
|
|
2469
|
-
(entry.loose_shown? '
|
|
2479
|
+
(entry.loose_shown? ' pressed_state' : ''),
|
|
2470
2480
|
type: 'button', style: 'padding:0 .45rem;',
|
|
2471
2481
|
title: t('loose records'), 'data-i18n-title': 'loose records',
|
|
2472
2482
|
'aria-label': t('loose records'),
|
|
@@ -2483,7 +2493,7 @@ function refresh_legend(gobj)
|
|
|
2483
2493
|
if(!hidden) {
|
|
2484
2494
|
extras.push(['button', {
|
|
2485
2495
|
class: 'GRAPH_LEGEND_FOCUS button is-small' +
|
|
2486
|
-
(focused? '
|
|
2496
|
+
(focused? ' pressed_state' : ''),
|
|
2487
2497
|
type: 'button', style: 'padding:0 .4rem;',
|
|
2488
2498
|
title: t('highlight topic'), 'data-i18n-title': 'highlight topic',
|
|
2489
2499
|
'aria-label': t('highlight topic'),
|
|
@@ -3479,6 +3479,8 @@ function ac_paste_rows(gobj, event, kw, src)
|
|
|
3479
3479
|
************************************************************/
|
|
3480
3480
|
function ac_form_save_record(gobj, event, kw, src)
|
|
3481
3481
|
{
|
|
3482
|
+
let priv = gobj.priv;
|
|
3483
|
+
|
|
3482
3484
|
if(refuse_if_readonly(gobj, event)) {
|
|
3483
3485
|
return -1; /* Error already logged */
|
|
3484
3486
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* priv_declared.test.js
|
|
3
|
+
*
|
|
4
|
+
* Every function that reads `priv` must have declared it.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A TEST AND NOT A REVIEW HABIT: a missing
|
|
7
|
+
* `let priv = gobj.priv;` is a ReferenceError that throws only
|
|
8
|
+
* when its LINE runs, and the line that reads `priv` is usually
|
|
9
|
+
* the first line of a path nobody walks every day. One shipped in
|
|
10
|
+
* `ac_form_save_record()` (7.23.64, the "form is busy while it
|
|
11
|
+
* reads the picked files" guard) and it meant that for three
|
|
12
|
+
* releases NO record could be saved from a treedb form dialog, in
|
|
13
|
+
* any consumer — the record travelled, the action threw before
|
|
14
|
+
* writing it, and the dialog just stayed open.
|
|
15
|
+
*
|
|
16
|
+
* The rule is the one every gclass of this repo already follows:
|
|
17
|
+
* the function that uses `priv` opens with `let priv = gobj.priv;`,
|
|
18
|
+
* or takes it as an argument, or is nested inside one that did.
|
|
19
|
+
*
|
|
20
|
+
* Copyright (c) 2026, ArtGins.
|
|
21
|
+
* All Rights Reserved.
|
|
22
|
+
***********************************************************************/
|
|
23
|
+
import {describe, it, expect} from "vitest";
|
|
24
|
+
import {readdirSync, readFileSync} from "node:fs";
|
|
25
|
+
import {dirname, join} from "node:path";
|
|
26
|
+
import {fileURLToPath} from "node:url";
|
|
27
|
+
|
|
28
|
+
const SRC = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/***************************************************************
|
|
32
|
+
* The source with its comments blanked out, line count kept.
|
|
33
|
+
*
|
|
34
|
+
* A doc block that SHOWS the idiom (`priv.theme_observer = ...`)
|
|
35
|
+
* is not a use of it, and every early version of this check
|
|
36
|
+
* reported four of those before the one real bug.
|
|
37
|
+
***************************************************************/
|
|
38
|
+
function without_comments(src)
|
|
39
|
+
{
|
|
40
|
+
let out = "";
|
|
41
|
+
let i = 0;
|
|
42
|
+
let state = "code"; /* code | line | block | s | d | t */
|
|
43
|
+
|
|
44
|
+
while(i < src.length) {
|
|
45
|
+
const c = src[i];
|
|
46
|
+
const d = src[i + 1];
|
|
47
|
+
|
|
48
|
+
if(state === "code") {
|
|
49
|
+
if(c === "/" && d === "/") {
|
|
50
|
+
state = "line";
|
|
51
|
+
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if(c === "/" && d === "*") {
|
|
55
|
+
state = "block";
|
|
56
|
+
out += " ";
|
|
57
|
+
i += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if(c === "'") {
|
|
61
|
+
state = "s";
|
|
62
|
+
} else if(c === '"') {
|
|
63
|
+
state = "d";
|
|
64
|
+
} else if(c === "`") {
|
|
65
|
+
state = "t";
|
|
66
|
+
}
|
|
67
|
+
out += c;
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if(state === "line") {
|
|
73
|
+
if(c === "\n") {
|
|
74
|
+
state = "code";
|
|
75
|
+
out += c;
|
|
76
|
+
}
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if(state === "block") {
|
|
82
|
+
if(c === "*" && d === "/") {
|
|
83
|
+
state = "code";
|
|
84
|
+
out += " ";
|
|
85
|
+
i += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
out += (c === "\n") ? "\n" : " ";
|
|
89
|
+
i++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Inside a string: it ends at its own quote, and a backslash
|
|
94
|
+
* escapes whatever follows. */
|
|
95
|
+
if(c === "\\") {
|
|
96
|
+
out += " ";
|
|
97
|
+
i += 2;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if((state === "s" && c === "'") || (state === "d" && c === '"')
|
|
101
|
+
|| (state === "t" && c === "`")) {
|
|
102
|
+
state = "code";
|
|
103
|
+
}
|
|
104
|
+
out += c;
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/***************************************************************
|
|
113
|
+
* Every top-level function of a file, as {name, line, params,
|
|
114
|
+
* body}. A nested function belongs to the body of the one that
|
|
115
|
+
* holds it, which is what lets it use the parent's `priv`.
|
|
116
|
+
***************************************************************/
|
|
117
|
+
function top_level_functions(src)
|
|
118
|
+
{
|
|
119
|
+
const lines = src.split("\n");
|
|
120
|
+
const starts = [];
|
|
121
|
+
for(let i = 0; i < lines.length; i++) {
|
|
122
|
+
if(/^(?:async )?function\s+\w+/.test(lines[i])) {
|
|
123
|
+
starts.push(i);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
starts.push(lines.length);
|
|
127
|
+
|
|
128
|
+
const out = [];
|
|
129
|
+
for(let k = 0; k < starts.length - 1; k++) {
|
|
130
|
+
const a = starts[k];
|
|
131
|
+
const m = /^(?:async )?function\s+(\w+)\s*\(([^)]*)/.exec(lines[a]);
|
|
132
|
+
if(!m) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out.push({
|
|
136
|
+
name: m[1],
|
|
137
|
+
line: a + 1,
|
|
138
|
+
params: m[2],
|
|
139
|
+
body: lines.slice(a, starts[k + 1]).join("\n"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
describe("every function that reads `priv` declares it", () => {
|
|
147
|
+
|
|
148
|
+
it("no bare `priv.` without `priv =` in the same function", () => {
|
|
149
|
+
const files = readdirSync(SRC)
|
|
150
|
+
.filter((f) => f.endsWith(".js") && !f.endsWith(".test.js"));
|
|
151
|
+
expect(files.length).toBeGreaterThan(10);
|
|
152
|
+
|
|
153
|
+
const offenders = [];
|
|
154
|
+
for(const f of files) {
|
|
155
|
+
const src = without_comments(readFileSync(join(SRC, f), "utf8"));
|
|
156
|
+
for(const fn of top_level_functions(src)) {
|
|
157
|
+
if(/(?<![.\w])priv(?![\w])/.test(fn.params)) {
|
|
158
|
+
continue; /* it takes priv as an argument */
|
|
159
|
+
}
|
|
160
|
+
const uses = /(?<![.\w])priv\s*\./.test(fn.body);
|
|
161
|
+
const declared = /(?<![.\w])priv\s*=[^=]/.test(fn.body);
|
|
162
|
+
if(uses && !declared) {
|
|
163
|
+
offenders.push(`${f}:${fn.line} ${fn.name}()`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect(offenders).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("the checker sees a missing declaration", () => {
|
|
172
|
+
/* The guard needs its own guard: a check that cannot fail is
|
|
173
|
+
* a check nobody can trust. */
|
|
174
|
+
const src = [
|
|
175
|
+
"function ac_broken(gobj, event, kw, src)",
|
|
176
|
+
"{",
|
|
177
|
+
" if(priv.reading_files) {",
|
|
178
|
+
" return -1;",
|
|
179
|
+
" }",
|
|
180
|
+
" return 0;",
|
|
181
|
+
"}",
|
|
182
|
+
].join("\n");
|
|
183
|
+
const fns = top_level_functions(without_comments(src));
|
|
184
|
+
expect(fns.length).toBe(1);
|
|
185
|
+
expect(/(?<![.\w])priv\s*\./.test(fns[0].body)).toBe(true);
|
|
186
|
+
expect(/(?<![.\w])priv\s*=[^=]/.test(fns[0].body)).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("a doc block that shows the idiom is not a use of it", () => {
|
|
190
|
+
const src = [
|
|
191
|
+
"/* priv.theme_observer = yui_watch_theme(gobj); */",
|
|
192
|
+
"function yui_is_dark()",
|
|
193
|
+
"{",
|
|
194
|
+
" return yui_theme_now() === 'dark';",
|
|
195
|
+
"}",
|
|
196
|
+
].join("\n");
|
|
197
|
+
const fns = top_level_functions(without_comments(src));
|
|
198
|
+
expect(fns.length).toBe(1);
|
|
199
|
+
expect(/(?<![.\w])priv\s*\./.test(fns[0].body)).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("`gobj.priv.x` is not a bare `priv`", () => {
|
|
203
|
+
const src = [
|
|
204
|
+
"function reads_through_gobj(gobj)",
|
|
205
|
+
"{",
|
|
206
|
+
" return gobj.priv.something;",
|
|
207
|
+
"}",
|
|
208
|
+
].join("\n");
|
|
209
|
+
const fns = top_level_functions(without_comments(src));
|
|
210
|
+
expect(/(?<![.\w])priv\s*\./.test(fns[0].body)).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
package/src/treedb_layout.js
CHANGED
|
@@ -142,6 +142,32 @@ export function spanning_tree(nodes, edges)
|
|
|
142
142
|
return {roots: roots, children: children, depth: depth, by_id: by_id};
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/************************************************************
|
|
146
|
+
* The ids of a spanning tree in PARENT-BEFORE-CHILD order.
|
|
147
|
+
*
|
|
148
|
+
* Every pass below used to be a recursive walk, which is the natural
|
|
149
|
+
* way to write it and the one that dies on a deep tree: a
|
|
150
|
+
* self-referent hook (a place inside a place inside a place) is as
|
|
151
|
+
* deep as the data says, and the stack is not. Read this list
|
|
152
|
+
* forwards for a pre-order pass and BACKWARDS for a post-order one
|
|
153
|
+
* -- a child always sits after its parent, so reversed it always
|
|
154
|
+
* sits before it, which is the whole of what post-order was for.
|
|
155
|
+
************************************************************/
|
|
156
|
+
function walk_order(t)
|
|
157
|
+
{
|
|
158
|
+
let order = [];
|
|
159
|
+
let stack = t.roots.slice().reverse();
|
|
160
|
+
while(stack.length) {
|
|
161
|
+
let id = stack.pop();
|
|
162
|
+
order.push(id);
|
|
163
|
+
let kids = t.children.get(id) || [];
|
|
164
|
+
for(let i = kids.length - 1; i >= 0; i--) {
|
|
165
|
+
stack.push(kids[i]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return order;
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
/************************************************************
|
|
146
172
|
* The tidy tree, left to right.
|
|
147
173
|
*
|
|
@@ -167,60 +193,54 @@ export function layout_tree(nodes, edges, opts)
|
|
|
167
193
|
x += (col_w[d] || 0) + o.ranksep;
|
|
168
194
|
}
|
|
169
195
|
|
|
170
|
-
/* Subtree heights
|
|
196
|
+
/* Subtree heights: post-order, so children before parents. */
|
|
197
|
+
let order = walk_order(t);
|
|
171
198
|
let height = new Map();
|
|
172
|
-
let
|
|
199
|
+
for(let i = order.length - 1; i >= 0; i--) {
|
|
200
|
+
let id = order[i];
|
|
173
201
|
let kids = t.children.get(id) || [];
|
|
174
202
|
let own = t.by_id.get(id).h;
|
|
175
|
-
if(!kids.length) {
|
|
176
|
-
height.set(id, own);
|
|
177
|
-
return own;
|
|
178
|
-
}
|
|
179
203
|
let sum = 0;
|
|
180
|
-
for(let
|
|
181
|
-
sum +=
|
|
182
|
-
if(
|
|
204
|
+
for(let k = 0; k < kids.length; k++) {
|
|
205
|
+
sum += height.get(kids[k]);
|
|
206
|
+
if(k > 0) {
|
|
183
207
|
sum += o.nodesep;
|
|
184
208
|
}
|
|
185
209
|
}
|
|
186
|
-
|
|
187
|
-
height.set(id, h);
|
|
188
|
-
return h;
|
|
189
|
-
};
|
|
190
|
-
for(let root of t.roots) {
|
|
191
|
-
measure(root);
|
|
210
|
+
height.set(id, kids.length? Math.max(own, sum) : own);
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
/* Placement, pre-order: a node is centred on the block of its
|
|
195
|
-
* children, which starts at the top of its own block.
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
* children, which starts at the top of its own block. Every node
|
|
215
|
+
* in a column is centred on the column, whatever its own width --
|
|
216
|
+
* the cards of one depth line up by their middles. */
|
|
217
|
+
let tops = new Map();
|
|
218
|
+
let top = 0;
|
|
219
|
+
for(let root of t.roots) {
|
|
220
|
+
tops.set(root, top);
|
|
221
|
+
top += height.get(root) + o.nodesep * 2;
|
|
222
|
+
}
|
|
223
|
+
for(let id of order) {
|
|
198
224
|
let d = t.depth.get(id);
|
|
199
225
|
let kids = t.children.get(id) || [];
|
|
200
226
|
let block = height.get(id);
|
|
201
|
-
let
|
|
227
|
+
let own_top = tops.get(id);
|
|
228
|
+
pos.set(id, {x: col_x[d] + col_w[d] / 2, y: own_top + block / 2});
|
|
202
229
|
if(!kids.length) {
|
|
203
|
-
|
|
204
|
-
return;
|
|
230
|
+
continue;
|
|
205
231
|
}
|
|
206
232
|
let kids_h = 0;
|
|
207
|
-
for(let
|
|
208
|
-
kids_h += height.get(kids[
|
|
209
|
-
if(
|
|
233
|
+
for(let k = 0; k < kids.length; k++) {
|
|
234
|
+
kids_h += height.get(kids[k]);
|
|
235
|
+
if(k > 0) {
|
|
210
236
|
kids_h += o.nodesep;
|
|
211
237
|
}
|
|
212
238
|
}
|
|
213
|
-
let cursor =
|
|
239
|
+
let cursor = own_top + (block - kids_h) / 2;
|
|
214
240
|
for(let kid of kids) {
|
|
215
|
-
|
|
241
|
+
tops.set(kid, cursor);
|
|
216
242
|
cursor += height.get(kid) + o.nodesep;
|
|
217
243
|
}
|
|
218
|
-
pos.set(id, {x: cx, y: top + block / 2});
|
|
219
|
-
};
|
|
220
|
-
let top = 0;
|
|
221
|
-
for(let root of t.roots) {
|
|
222
|
-
place(root, top);
|
|
223
|
-
top += height.get(root) + o.nodesep * 2;
|
|
224
244
|
}
|
|
225
245
|
|
|
226
246
|
return pos;
|
|
@@ -253,24 +273,25 @@ export function layout_radial(nodes, edges, opts)
|
|
|
253
273
|
return pos;
|
|
254
274
|
}
|
|
255
275
|
|
|
256
|
-
/* Leaves under each node: the sector it deserves. */
|
|
276
|
+
/* Leaves under each node: the sector it deserves. Post-order. */
|
|
277
|
+
let order = walk_order(t);
|
|
257
278
|
let leaves = new Map();
|
|
258
|
-
let
|
|
279
|
+
for(let i = order.length - 1; i >= 0; i--) {
|
|
280
|
+
let id = order[i];
|
|
259
281
|
let kids = t.children.get(id) || [];
|
|
260
282
|
if(!kids.length) {
|
|
261
283
|
leaves.set(id, 1);
|
|
262
|
-
|
|
284
|
+
continue;
|
|
263
285
|
}
|
|
264
286
|
let n = 0;
|
|
265
287
|
for(let kid of kids) {
|
|
266
|
-
n +=
|
|
288
|
+
n += leaves.get(kid);
|
|
267
289
|
}
|
|
268
290
|
leaves.set(id, n);
|
|
269
|
-
|
|
270
|
-
};
|
|
291
|
+
}
|
|
271
292
|
let total = 0;
|
|
272
293
|
for(let root of t.roots) {
|
|
273
|
-
total +=
|
|
294
|
+
total += leaves.get(root);
|
|
274
295
|
}
|
|
275
296
|
|
|
276
297
|
/* One root sits at the centre; several sit on the first ring
|
|
@@ -283,20 +304,27 @@ export function layout_radial(nodes, edges, opts)
|
|
|
283
304
|
* Angles do not depend on the radii, so they come first. */
|
|
284
305
|
let angle = new Map();
|
|
285
306
|
let rings = []; /* ring -> [{id, angle, diag}] */
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
/* An explicit stack, not recursion: the sectors of a ring are
|
|
308
|
+
* sorted by angle in pass 2, so the order they are pushed in does
|
|
309
|
+
* not matter -- only that every node gets its own. */
|
|
310
|
+
let sector = (root_id, root_from, root_span) => {
|
|
311
|
+
let stack = [{id: root_id, from: root_from, span: root_span}];
|
|
312
|
+
while(stack.length) {
|
|
313
|
+
let {id, from, span} = stack.pop();
|
|
314
|
+
let a = from + span / 2;
|
|
315
|
+
angle.set(id, a);
|
|
316
|
+
let ring = ring_of(id);
|
|
317
|
+
if(!rings[ring]) {
|
|
318
|
+
rings[ring] = [];
|
|
319
|
+
}
|
|
320
|
+
let n = t.by_id.get(id);
|
|
321
|
+
rings[ring].push({id: id, angle: a, diag: Math.hypot(n.w, n.h)});
|
|
322
|
+
let cursor = from;
|
|
323
|
+
for(let kid of (t.children.get(id) || [])) {
|
|
324
|
+
let part = span * leaves.get(kid) / leaves.get(id);
|
|
325
|
+
stack.push({id: kid, from: cursor, span: part});
|
|
326
|
+
cursor += part;
|
|
327
|
+
}
|
|
300
328
|
}
|
|
301
329
|
};
|
|
302
330
|
if(single) {
|
|
@@ -362,13 +390,20 @@ export function layout_outline(nodes, edges, opts)
|
|
|
362
390
|
let pos = new Map();
|
|
363
391
|
|
|
364
392
|
let y = 0;
|
|
365
|
-
let visit = (
|
|
366
|
-
let
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
393
|
+
let visit = (root_id) => {
|
|
394
|
+
let stack = [root_id];
|
|
395
|
+
while(stack.length) {
|
|
396
|
+
let id = stack.pop();
|
|
397
|
+
let n = t.by_id.get(id);
|
|
398
|
+
let d = t.depth.get(id);
|
|
399
|
+
pos.set(id, {x: d * o.indent + n.w / 2, y: y + n.h / 2});
|
|
400
|
+
y += n.h + o.nodesep;
|
|
401
|
+
/* Reversed, so a sibling pops in the order it was given:
|
|
402
|
+
* here the order of the walk IS the order of the rows. */
|
|
403
|
+
let kids = t.children.get(id) || [];
|
|
404
|
+
for(let i = kids.length - 1; i >= 0; i--) {
|
|
405
|
+
stack.push(kids[i]);
|
|
406
|
+
}
|
|
372
407
|
}
|
|
373
408
|
};
|
|
374
409
|
for(let root of t.roots) {
|
|
@@ -170,3 +170,35 @@ describe("the outline", () => {
|
|
|
170
170
|
expect(pos.get("norte").y).toBe(96 + 10 + 48);
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
describe("a deep tree", () => {
|
|
175
|
+
/* A self-referent hook -- a place inside a place inside a place --
|
|
176
|
+
* is as deep as the data says. The walks are iterative for exactly
|
|
177
|
+
* this: recursive ones died here with "Maximum call stack size
|
|
178
|
+
* exceeded", and a graph that cannot be drawn is not a layout
|
|
179
|
+
* choice, it is an exception in the console. */
|
|
180
|
+
const DEEP = 20000;
|
|
181
|
+
const deep_nodes = [];
|
|
182
|
+
const deep_edges = [];
|
|
183
|
+
for(let i = 0; i < DEEP; i++) {
|
|
184
|
+
deep_nodes.push({id: `n${i}`, w: 100, h: 40});
|
|
185
|
+
if(i > 0) {
|
|
186
|
+
deep_edges.push({source: `n${i - 1}`, target: `n${i}`, rank: 0});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test("the spanning tree reaches the bottom", () => {
|
|
191
|
+
let t = spanning_tree(deep_nodes, deep_edges);
|
|
192
|
+
expect(t.roots).toEqual(["n0"]);
|
|
193
|
+
expect(t.depth.get(`n${DEEP - 1}`)).toBe(DEEP - 1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("the three layouts place every node of it", () => {
|
|
197
|
+
for(let layout of [layout_tree, layout_outline, layout_radial]) {
|
|
198
|
+
let pos = layout(deep_nodes, deep_edges);
|
|
199
|
+
expect(pos.size).toBe(DEEP);
|
|
200
|
+
expect(Number.isFinite(pos.get(`n${DEEP - 1}`).x)).toBe(true);
|
|
201
|
+
expect(Number.isFinite(pos.get(`n${DEEP - 1}`).y)).toBe(true);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|