archaeopteryx 3.14.0 → 3.16.0
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 +166 -18
- package/archaeopteryx.js +1248 -98
- package/forester.js +353 -3
- package/package.json +4 -2
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.16.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3814,6 +3814,349 @@
|
|
|
3814
3814
|
};
|
|
3815
3815
|
|
|
3816
3816
|
|
|
3817
|
+
// The nodes of `list` in PREORDER -- root first, then each subtree -- which
|
|
3818
|
+
// is the claim order the crowded-data rule needs and is NOT the order the
|
|
3819
|
+
// layout hands out. d3's descendants() is BREADTH-first (r,a,b,a1,a2,b1),
|
|
3820
|
+
// and the viewer reverses it for drawing, which puts the root LAST. Taking
|
|
3821
|
+
// that order for preorder inverts the whole rule: the leaf-most mark wins
|
|
3822
|
+
// and the one nearer the root is dropped, which is the opposite of what
|
|
3823
|
+
// the desktop does. Measured, after a review caught it: "descendants()
|
|
3824
|
+
// r,a,b,a1,a2,b1" and "reversed b1,a2,a1,b,a,r".
|
|
3825
|
+
//
|
|
3826
|
+
// Derived from the node objects rather than from any layout, so it cannot
|
|
3827
|
+
// drift with one: the root is the member whose parent is not itself in the
|
|
3828
|
+
// list, and children are visited in their own order. A node whose parent
|
|
3829
|
+
// is absent (a subtree view) roots its own walk, and anything unreachable
|
|
3830
|
+
// is appended so no mark is silently skipped.
|
|
3831
|
+
forester.preorderOf = function (list) {
|
|
3832
|
+
let present = new Set(list);
|
|
3833
|
+
let out = [];
|
|
3834
|
+
let seen = new Set();
|
|
3835
|
+
function walk(n) {
|
|
3836
|
+
if (seen.has(n)) {
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
seen.add(n);
|
|
3840
|
+
out.push(n);
|
|
3841
|
+
let kids = n.children;
|
|
3842
|
+
if (kids) {
|
|
3843
|
+
for (let i = 0; i < kids.length; ++i) {
|
|
3844
|
+
if (present.has(kids[i])) {
|
|
3845
|
+
walk(kids[i]);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
for (let i = 0; i < list.length; ++i) {
|
|
3851
|
+
let n = list[i];
|
|
3852
|
+
if (!n.parent || !present.has(n.parent)) {
|
|
3853
|
+
walk(n);
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
for (let i = 0; i < list.length; ++i) {
|
|
3857
|
+
if (!seen.has(list[i])) {
|
|
3858
|
+
out.push(list[i]); // a cycle or a detached node: never dropped
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return out;
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3864
|
+
// The box to claim for a mark that RIDES ITS BRANCH -- the circular and
|
|
3865
|
+
// unrooted layouts, where a label is turned to the branch's direction
|
|
3866
|
+
// instead of standing along a row. The axis-aligned bounds of the turned
|
|
3867
|
+
// rectangle, centred on (cx, cy):
|
|
3868
|
+
//
|
|
3869
|
+
// rot_w = |w*cos(a)| + |h*sin(a)|
|
|
3870
|
+
// rot_h = |w*sin(a)| + |h*cos(a)|
|
|
3871
|
+
//
|
|
3872
|
+
// The desktop's formula, verbatim (TreePanel.paintBranchDataRadial), and
|
|
3873
|
+
// the second half of the joint rule below. It carries between the two
|
|
3874
|
+
// programs because both measure it in real SCREEN space -- the layout's
|
|
3875
|
+
// own units do not survive the trip, which is why this was parked as a
|
|
3876
|
+
// divergence until forester-71 sent the arithmetic.
|
|
3877
|
+
//
|
|
3878
|
+
// Deliberately conservative: a diagonal label's bounds are wider than its
|
|
3879
|
+
// ink, so a crowded number is dropped rather than overprinted. Exact at
|
|
3880
|
+
// the right angles, worst at 45 degrees. Insensitive to the sign of the
|
|
3881
|
+
// angle and to a half turn, which is what lets the 180-degree flip that
|
|
3882
|
+
// keeps the far half of a fan upright leave the box alone.
|
|
3883
|
+
forester.rotatedLabelBox = function (cx, cy, w, h, angle) {
|
|
3884
|
+
let cos = Math.abs(Math.cos(angle));
|
|
3885
|
+
let sin = Math.abs(Math.sin(angle));
|
|
3886
|
+
let rw = (w * cos) + (h * sin);
|
|
3887
|
+
let rh = (w * sin) + (h * cos);
|
|
3888
|
+
return [cx - (rw / 2), cy - (rh / 2), rw, rh];
|
|
3889
|
+
};
|
|
3890
|
+
|
|
3891
|
+
// The occupancy map behind "auto-hide crowded branch data": a mark is
|
|
3892
|
+
// drawn only when its box overlaps nothing already granted in the same
|
|
3893
|
+
// pass. Ported from the desktop's LabelOccupancy (0.11.161) as a JOINT
|
|
3894
|
+
// rule on Christian's word, 2026-09-25, so neither side retunes it alone.
|
|
3895
|
+
//
|
|
3896
|
+
// Here rather than in the viewer because it is a pure algorithm and the
|
|
3897
|
+
// suite can reach it: the parts that MUST match the desktop -- first claim
|
|
3898
|
+
// wins, a refused claim records nothing, a zero-size box is granted and
|
|
3899
|
+
// not recorded, and boxes that merely touch do not overlap -- are exactly
|
|
3900
|
+
// the parts no screenshot would show going wrong.
|
|
3901
|
+
// A uniform grid rather than all-pairs: a 13,246-tip tree with branch
|
|
3902
|
+
// lengths on offers 18,511 boxes, and 171 million comparisons per redraw
|
|
3903
|
+
// is not a redraw.
|
|
3904
|
+
/**
|
|
3905
|
+
* The same first-come space-keeping as labelOccupancy, but for boxes that
|
|
3906
|
+
* are TURNED: each claim is a quadrilateral, and two of them overlap only
|
|
3907
|
+
* if their own outlines do.
|
|
3908
|
+
*
|
|
3909
|
+
* A turned label cannot use the axis-aligned map. The bounds of a rotated
|
|
3910
|
+
* rectangle grow with its length, so a long name lying near its spoke
|
|
3911
|
+
* reserves several times its own area, and the map refuses neighbours that
|
|
3912
|
+
* a reader can see are clear of it. Measured on a 48-label circular ring:
|
|
3913
|
+
* 28 pairs "overlapping" by the bounds where the outlines gave 0, and a
|
|
3914
|
+
* first-come pass over the same labels in the same order kept 48 by
|
|
3915
|
+
* outline against 32 by bounds -- sixteen names dropped for nothing.
|
|
3916
|
+
*
|
|
3917
|
+
* The grid is still used, on each quad's bounds, to find which claims are
|
|
3918
|
+
* worth testing; only the test itself is exact.
|
|
3919
|
+
*
|
|
3920
|
+
* @param cell - grid size, in the same units as the quads
|
|
3921
|
+
* @returns an object with claim(quad) and occupy(quad); a quad is four
|
|
3922
|
+
* [x, y] corners in order round the shape
|
|
3923
|
+
*/
|
|
3924
|
+
forester.orientedOccupancy = function (cell) {
|
|
3925
|
+
let size = Math.max(4, cell);
|
|
3926
|
+
let cells = new Map();
|
|
3927
|
+
let boundsOf = function (quad) {
|
|
3928
|
+
let x0 = quad[0][0], x1 = quad[0][0], y0 = quad[0][1], y1 = quad[0][1];
|
|
3929
|
+
for (let i = 1; i < quad.length; ++i) {
|
|
3930
|
+
x0 = Math.min(x0, quad[i][0]);
|
|
3931
|
+
x1 = Math.max(x1, quad[i][0]);
|
|
3932
|
+
y0 = Math.min(y0, quad[i][1]);
|
|
3933
|
+
y1 = Math.max(y1, quad[i][1]);
|
|
3934
|
+
}
|
|
3935
|
+
return [x0, y0, x1, y1];
|
|
3936
|
+
};
|
|
3937
|
+
// Separating-axis test. A rectangle's own two edge normals are enough
|
|
3938
|
+
// for itself, but BOTH shapes' normals are needed: with only one
|
|
3939
|
+
// shape's, a box that clears another along the OTHER's axis is
|
|
3940
|
+
// reported as overlapping. Strict, so shapes that merely touch do not
|
|
3941
|
+
// overlap -- as in labelOccupancy.
|
|
3942
|
+
let overlaps = function (P, Q) {
|
|
3943
|
+
for (let s = 0; s < 2; ++s) {
|
|
3944
|
+
let R = s === 0 ? P : Q;
|
|
3945
|
+
for (let k = 0; k < 2; ++k) {
|
|
3946
|
+
let ax = R[k + 1][1] - R[k][1];
|
|
3947
|
+
let ay = R[k][0] - R[k + 1][0];
|
|
3948
|
+
let p0 = Infinity, p1 = -Infinity, q0 = Infinity, q1 = -Infinity;
|
|
3949
|
+
for (let i = 0; i < P.length; ++i) {
|
|
3950
|
+
let d = (P[i][0] * ax) + (P[i][1] * ay);
|
|
3951
|
+
p0 = Math.min(p0, d);
|
|
3952
|
+
p1 = Math.max(p1, d);
|
|
3953
|
+
}
|
|
3954
|
+
for (let i = 0; i < Q.length; ++i) {
|
|
3955
|
+
let d = (Q[i][0] * ax) + (Q[i][1] * ay);
|
|
3956
|
+
q0 = Math.min(q0, d);
|
|
3957
|
+
q1 = Math.max(q1, d);
|
|
3958
|
+
}
|
|
3959
|
+
if (p1 <= q0 || q1 <= p0) {
|
|
3960
|
+
return false;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
return true;
|
|
3965
|
+
};
|
|
3966
|
+
let record = function (quad, b) {
|
|
3967
|
+
let c1 = Math.floor(b[2] / size);
|
|
3968
|
+
let r1 = Math.floor(b[3] / size);
|
|
3969
|
+
for (let c = Math.floor(b[0] / size); c <= c1; ++c) {
|
|
3970
|
+
for (let r = Math.floor(b[1] / size); r <= r1; ++r) {
|
|
3971
|
+
let key = c + ':' + r;
|
|
3972
|
+
let bucket = cells.get(key);
|
|
3973
|
+
if (!bucket) {
|
|
3974
|
+
bucket = [];
|
|
3975
|
+
cells.set(key, bucket);
|
|
3976
|
+
}
|
|
3977
|
+
bucket.push(quad);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
};
|
|
3981
|
+
let free = function (quad, b) {
|
|
3982
|
+
let seen = null;
|
|
3983
|
+
let c1 = Math.floor(b[2] / size);
|
|
3984
|
+
let r1 = Math.floor(b[3] / size);
|
|
3985
|
+
for (let c = Math.floor(b[0] / size); c <= c1; ++c) {
|
|
3986
|
+
for (let r = Math.floor(b[1] / size); r <= r1; ++r) {
|
|
3987
|
+
let bucket = cells.get(c + ':' + r);
|
|
3988
|
+
if (!bucket) {
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
for (let i = 0; i < bucket.length; ++i) {
|
|
3992
|
+
// a quad spans several cells, so the same one comes up
|
|
3993
|
+
// more than once; the test is the expensive part
|
|
3994
|
+
if (seen === null) {
|
|
3995
|
+
seen = new Set();
|
|
3996
|
+
}
|
|
3997
|
+
if (seen.has(bucket[i])) {
|
|
3998
|
+
continue;
|
|
3999
|
+
}
|
|
4000
|
+
seen.add(bucket[i]);
|
|
4001
|
+
if (overlaps(quad, bucket[i])) {
|
|
4002
|
+
return false;
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return true;
|
|
4008
|
+
};
|
|
4009
|
+
let degenerate = function (quad) {
|
|
4010
|
+
if (!quad || quad.length !== 4) {
|
|
4011
|
+
return true;
|
|
4012
|
+
}
|
|
4013
|
+
let b = boundsOf(quad);
|
|
4014
|
+
return !((b[2] - b[0]) > 0) || !((b[3] - b[1]) > 0);
|
|
4015
|
+
};
|
|
4016
|
+
return {
|
|
4017
|
+
claim: function (quad) {
|
|
4018
|
+
if (degenerate(quad)) {
|
|
4019
|
+
return true; // reserves nothing, as a zero-size box does
|
|
4020
|
+
}
|
|
4021
|
+
let b = boundsOf(quad);
|
|
4022
|
+
if (!free(quad, b)) {
|
|
4023
|
+
return false;
|
|
4024
|
+
}
|
|
4025
|
+
record(quad, b);
|
|
4026
|
+
return true;
|
|
4027
|
+
},
|
|
4028
|
+
// record without asking, for what is drawn whatever else is there
|
|
4029
|
+
occupy: function (quad) {
|
|
4030
|
+
if (degenerate(quad)) {
|
|
4031
|
+
return true;
|
|
4032
|
+
}
|
|
4033
|
+
let b = boundsOf(quad);
|
|
4034
|
+
let clear = free(quad, b);
|
|
4035
|
+
record(quad, b);
|
|
4036
|
+
return clear;
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
};
|
|
4040
|
+
|
|
4041
|
+
forester.labelOccupancy = function (cell) {
|
|
4042
|
+
let size = Math.max(4, cell);
|
|
4043
|
+
let cells = new Map();
|
|
4044
|
+
let record = function (x, y, w, h) {
|
|
4045
|
+
let box = [x, y, w, h];
|
|
4046
|
+
let c1 = Math.floor((x + w) / size);
|
|
4047
|
+
let r1 = Math.floor((y + h) / size);
|
|
4048
|
+
for (let c = Math.floor(x / size); c <= c1; ++c) {
|
|
4049
|
+
for (let r = Math.floor(y / size); r <= r1; ++r) {
|
|
4050
|
+
let key = c + ':' + r;
|
|
4051
|
+
let bucket = cells.get(key);
|
|
4052
|
+
if (!bucket) {
|
|
4053
|
+
bucket = [];
|
|
4054
|
+
cells.set(key, bucket);
|
|
4055
|
+
}
|
|
4056
|
+
bucket.push(box);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
let free = function (x, y, w, h) {
|
|
4061
|
+
let c1 = Math.floor((x + w) / size);
|
|
4062
|
+
let r1 = Math.floor((y + h) / size);
|
|
4063
|
+
for (let c = Math.floor(x / size); c <= c1; ++c) {
|
|
4064
|
+
for (let r = Math.floor(y / size); r <= r1; ++r) {
|
|
4065
|
+
let bucket = cells.get(c + ':' + r);
|
|
4066
|
+
if (!bucket) {
|
|
4067
|
+
continue;
|
|
4068
|
+
}
|
|
4069
|
+
for (let i = 0; i < bucket.length; ++i) {
|
|
4070
|
+
let b = bucket[i];
|
|
4071
|
+
// strict: boxes that merely touch do not overlap
|
|
4072
|
+
if (x < (b[0] + b[2]) && (x + w) > b[0]
|
|
4073
|
+
&& y < (b[1] + b[3]) && (y + h) > b[1]) {
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
return true;
|
|
4080
|
+
};
|
|
4081
|
+
return {
|
|
4082
|
+
// true when nothing already granted overlaps, and then recorded
|
|
4083
|
+
claim: function (x, y, w, h) {
|
|
4084
|
+
if (!(w > 0) || !(h > 0)) {
|
|
4085
|
+
return true; // reserves no space; recording it would only cost comparisons
|
|
4086
|
+
}
|
|
4087
|
+
if (!free(x, y, w, h)) {
|
|
4088
|
+
return false;
|
|
4089
|
+
}
|
|
4090
|
+
record(x, y, w, h);
|
|
4091
|
+
return true;
|
|
4092
|
+
},
|
|
4093
|
+
// Record without asking, for what is drawn whatever else is there.
|
|
4094
|
+
// A refused CLAIM records nothing, so a label put through claim()
|
|
4095
|
+
// and refused would leave the space open and let a number print
|
|
4096
|
+
// straight through the name it lost to -- the opposite of the
|
|
4097
|
+
// point. Returns whether the space was clear, for measurement;
|
|
4098
|
+
// the box is recorded either way.
|
|
4099
|
+
occupy: function (x, y, w, h) {
|
|
4100
|
+
if (!(w > 0) || !(h > 0)) {
|
|
4101
|
+
return true;
|
|
4102
|
+
}
|
|
4103
|
+
let clear = free(x, y, w, h);
|
|
4104
|
+
record(x, y, w, h);
|
|
4105
|
+
return clear;
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
};
|
|
4109
|
+
|
|
4110
|
+
// Where a protein domain can be read about, as {url, exact} or null.
|
|
4111
|
+
//
|
|
4112
|
+
// An InterPro ENTRY is addressed by ACCESSION and by nothing else --
|
|
4113
|
+
// measured, not assumed: /api/entry/pfam/PF00931/ answers 200 and names
|
|
4114
|
+
// NB-ARC, while /api/entry/pfam/NB-ARC/, /CARD/ and /Death/ are all 404.
|
|
4115
|
+
// phyloXML's <domain id="..."> carries the accession when the annotation
|
|
4116
|
+
// had one, and most files have none (our own apaf.xml names all 166 of its
|
|
4117
|
+
// boxes and gives not one an id), so the fallback is an InterPro text
|
|
4118
|
+
// search.
|
|
4119
|
+
//
|
|
4120
|
+
// The search goes by NAME, never by the id. An id that is not a bare Pfam
|
|
4121
|
+
// accession -- a versioned one from an hmmscan (PF00931.24), or a SMART or
|
|
4122
|
+
// CDD accession -- searches to nothing, while the name is exactly what a
|
|
4123
|
+
// text search is for. A version suffix is stripped for the entry URL
|
|
4124
|
+
// rather than disqualifying it: PF00931.24 IS PF00931.
|
|
4125
|
+
//
|
|
4126
|
+
// Lives here rather than in the viewer so it can be tested: a wrong URL
|
|
4127
|
+
// fails silently in someone else's browser, which is the worst place to
|
|
4128
|
+
// find out.
|
|
4129
|
+
const INTERPRO_ENTRY = 'https://www.ebi.ac.uk/interpro/entry/pfam/';
|
|
4130
|
+
const INTERPRO_SEARCH = 'https://www.ebi.ac.uk/interpro/search/text/';
|
|
4131
|
+
|
|
4132
|
+
forester.domainReference = function (domain) {
|
|
4133
|
+
if (!domain) {
|
|
4134
|
+
return null;
|
|
4135
|
+
}
|
|
4136
|
+
let id = (domain.id || '').trim();
|
|
4137
|
+
let acc = /^(PF\d{5,})(?:\.\d+)?$/i.exec(id);
|
|
4138
|
+
if (acc) {
|
|
4139
|
+
return {url: INTERPRO_ENTRY + acc[1].toUpperCase() + '/', exact: true};
|
|
4140
|
+
}
|
|
4141
|
+
let q = (domain.name || '').trim() || id;
|
|
4142
|
+
return q ? {url: INTERPRO_SEARCH + encodeURIComponent(q) + '/', exact: false} : null;
|
|
4143
|
+
};
|
|
4144
|
+
|
|
4145
|
+
// A domain's E-value for display. It spans many orders of magnitude, so a
|
|
4146
|
+
// small one keeps its exponent; the count of significant digits is the
|
|
4147
|
+
// only thing normalized, because a scan writes 7.2E-117 and 1E-6 and a
|
|
4148
|
+
// readout that mixed those forms would read as two different measures.
|
|
4149
|
+
forester.domainEvalueText = function (e) {
|
|
4150
|
+
if (typeof e !== 'number' || !isFinite(e)) {
|
|
4151
|
+
return null;
|
|
4152
|
+
}
|
|
4153
|
+
if (e === 0) {
|
|
4154
|
+
return '0';
|
|
4155
|
+
}
|
|
4156
|
+
return (Math.abs(e) >= 0.001 && Math.abs(e) < 1e7)
|
|
4157
|
+
? String(forester.roundNumber(e, 6)) : e.toExponential(2);
|
|
4158
|
+
};
|
|
4159
|
+
|
|
3817
4160
|
// Everything the Tree Properties dialog reports, gathered in ONE pass:
|
|
3818
4161
|
// the desktop's TreeFacts.scan, minus its histogram (Christian, 2026-09-23:
|
|
3819
4162
|
// "we don't need a histogram ... but a more detailed stats would be
|
|
@@ -6933,7 +7276,13 @@
|
|
|
6933
7276
|
ignored++;
|
|
6934
7277
|
return;
|
|
6935
7278
|
}
|
|
6936
|
-
|
|
7279
|
+
// `id` is phyloXML's optional domain identifier, which for a Pfam
|
|
7280
|
+
// scan is the accession (PF00931). Carried through because the
|
|
7281
|
+
// hover readout links to the entry with it when it is there, and
|
|
7282
|
+
// has to search by name when it is not -- apaf.xml, our own domain
|
|
7283
|
+
// demo, has names and no ids at all.
|
|
7284
|
+
domains.push({name: d.name ? String(d.name) : '', from: from, to: to,
|
|
7285
|
+
length: to - from + 1, evalue: e, id: d.id ? String(d.id) : ''});
|
|
6937
7286
|
});
|
|
6938
7287
|
domains.sort(function (a, b) {
|
|
6939
7288
|
return a.from - b.from;
|
|
@@ -7035,7 +7384,8 @@
|
|
|
7035
7384
|
if (!(w > 0) || !isFinite(w)) {
|
|
7036
7385
|
return;
|
|
7037
7386
|
}
|
|
7038
|
-
boxes.push({name: d.name, x: start + ((d.from - 1) * f), w: w, from: d.from, to: d.to,
|
|
7387
|
+
boxes.push({name: d.name, x: start + ((d.from - 1) * f), w: w, from: d.from, to: d.to,
|
|
7388
|
+
evalue: d.evalue, id: d.id});
|
|
7039
7389
|
});
|
|
7040
7390
|
return {backbone: {x: start, w: Number(da.length) * f}, boxes: boxes};
|
|
7041
7391
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
|
|
5
5
|
"main": "archaeopteryx.js",
|
|
6
6
|
"types": "archaeopteryx.d.ts",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"lint:fix": "eslint . --fix",
|
|
26
26
|
"format": "prettier --write .",
|
|
27
27
|
"format:check": "prettier --check .",
|
|
28
|
-
"docs:sync": "cp archaeopteryx.js forester.js docs/lib/"
|
|
28
|
+
"docs:sync": "cp archaeopteryx.js forester.js docs/lib/",
|
|
29
|
+
"test:browser": "node test/browser/run.js",
|
|
30
|
+
"test:browser:quick": "node test/browser/run.js --quick"
|
|
29
31
|
},
|
|
30
32
|
"repository": {
|
|
31
33
|
"type": "git",
|