@figtreejs/core 0.0.1-alpha.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/CHANGELOG.md +9 -0
- package/eslint.config.js +9 -0
- package/package.json +76 -0
- package/src/@custom-types/normalize-svg-path.d.ts +13 -0
- package/src/@custom-types/parse-svg-path.d.ts +8 -0
- package/src/@custom-types/svg-path-types.d.ts +37 -0
- package/src/bauble-makers/makers.ts +112 -0
- package/src/bauble-makers/set-up-baubles.ts +197 -0
- package/src/bauble-makers/utils.ts +61 -0
- package/src/components/baubles/bauble.tsx +61 -0
- package/src/components/baubles/branches.tsx +13 -0
- package/src/components/baubles/clades/cartoon.tsx +68 -0
- package/src/components/baubles/clades/highlight.tsx +96 -0
- package/src/components/baubles/clades/index.ts +1 -0
- package/src/components/baubles/clades.tsx +45 -0
- package/src/components/baubles/helpers.tsx +62 -0
- package/src/components/baubles/index.ts +16 -0
- package/src/components/baubles/labels.tsx +38 -0
- package/src/components/baubles/nodes.tsx +51 -0
- package/src/components/baubles/shapes/branch.tsx +53 -0
- package/src/components/baubles/shapes/circle.tsx +64 -0
- package/src/components/baubles/shapes/index.ts +9 -0
- package/src/components/baubles/shapes/label.tsx +104 -0
- package/src/components/baubles/shapes/rectangle.tsx +83 -0
- package/src/components/baubles/types.ts +99 -0
- package/src/components/decorations/axis/axis-types.ts +123 -0
- package/src/components/decorations/axis/axis.tsx +21 -0
- package/src/components/decorations/axis/index.ts +2 -0
- package/src/components/decorations/axis/polar-axis-bars.tsx +102 -0
- package/src/components/decorations/axis/polar-axis.tsx +175 -0
- package/src/components/decorations/axis/rectangular-axis-bars.tsx +53 -0
- package/src/components/decorations/axis/rectangular-axis.tsx +151 -0
- package/src/components/decorations/index.ts +2 -0
- package/src/components/decorations/legend/discrete-legend.tsx +93 -0
- package/src/components/decorations/legend/index.ts +1 -0
- package/src/components/decorations/legend/legend.tsx +1 -0
- package/src/components/figtree/figtree-types.ts +69 -0
- package/src/components/figtree/figtree.tsx +136 -0
- package/src/components/figtree/index.ts +3 -0
- package/src/components/hoc/index.ts +7 -0
- package/src/components/hoc/with-branch.tsx +148 -0
- package/src/components/hoc/with-branches.tsx +54 -0
- package/src/components/hoc/with-clades.tsx +47 -0
- package/src/components/hoc/with-node.tsx +183 -0
- package/src/components/hoc/with-nodes.tsx +45 -0
- package/src/components/index.ts +4 -0
- package/src/context/aminated-context.ts +3 -0
- package/src/context/dimension-context.ts +22 -0
- package/src/context/layout-context.ts +20 -0
- package/src/context/scale-context.ts +12 -0
- package/src/evo/index.ts +1 -0
- package/src/evo/tree/index.ts +5 -0
- package/src/evo/tree/mcc-tree.ts +0 -0
- package/src/evo/tree/normalized-tree/immutable-tree-helpers.ts +136 -0
- package/src/evo/tree/normalized-tree/immutable-tree.test.ts +158 -0
- package/src/evo/tree/normalized-tree/immutable-tree.ts +1365 -0
- package/src/evo/tree/normalized-tree/index.ts +3 -0
- package/src/evo/tree/parsers/annotation-parser.ts +276 -0
- package/src/evo/tree/parsers/index.ts +3 -0
- package/src/evo/tree/parsers/newick-character-parser.ts +246 -0
- package/src/evo/tree/parsers/newick-parsing.ts +22 -0
- package/src/evo/tree/parsers/nexus-parser.ts +12 -0
- package/src/evo/tree/parsers/nexus-parsing.ts +68 -0
- package/src/evo/tree/parsers/parsing.test.ts +289 -0
- package/src/evo/tree/parsers/stream-reader/index.ts +1 -0
- package/src/evo/tree/parsers/stream-reader/newick-importer.txt +395 -0
- package/src/evo/tree/parsers/stream-reader/nexus-importer.test.ts +99 -0
- package/src/evo/tree/parsers/stream-reader/nexus-importer.ts +293 -0
- package/src/evo/tree/parsers/stream-reader/nexus-tokenizer.ts +77 -0
- package/src/evo/tree/parsers/stream-reader/nexus-transform-stream.txt +109 -0
- package/src/evo/tree/taxa/helper-functions.ts +46 -0
- package/src/evo/tree/taxa/index.ts +1 -0
- package/src/evo/tree/taxa/taxon.ts +116 -0
- package/src/evo/tree/traversals/index.ts +1 -0
- package/src/evo/tree/traversals/preorder-traversal.ts +89 -0
- package/src/evo/tree/traversals/traversal-types.ts +6 -0
- package/src/evo/tree/tree-types.ts +197 -0
- package/src/evo/tree/utilities.ts +44 -0
- package/src/index.ts +6 -0
- package/src/layouts/functional/index.ts +2 -0
- package/src/layouts/functional/radial-layout.ts +150 -0
- package/src/layouts/functional/rectangular-layout.ts +71 -0
- package/src/layouts/index.ts +3 -0
- package/src/layouts/layout-interface.ts +90 -0
- package/src/layouts/types.ts +32 -0
- package/src/path.helpers.ts +81 -0
- package/src/store/polar-scale.ts +145 -0
- package/src/store/store.ts +144 -0
- package/src/tests/baubles/__snapshots__/branch-labels.test.tsx.snap +901 -0
- package/src/tests/baubles/__snapshots__/node-labels.test.tsx.snap +1516 -0
- package/src/tests/baubles/branch-labels.test.tsx +103 -0
- package/src/tests/baubles/label.svg +131 -0
- package/src/tests/baubles/node-labels.test.tsx +126 -0
- package/src/tests/clades/__snapshots__/cartoon.test.tsx.snap +327 -0
- package/src/tests/clades/__snapshots__/highlight.test.tsx.snap +337 -0
- package/src/tests/clades/cartoon.test.tsx +65 -0
- package/src/tests/clades/highlight.test.tsx +66 -0
- package/src/tests/figtree/__snapshots__/figtree.test.tsx.snap +761 -0
- package/src/tests/figtree/figtree.test.tsx +123 -0
- package/src/tests/figtree/simple.svg +47 -0
- package/src/tests/layouts/radiallayout.test.ts +23 -0
- package/src/tests/layouts/rectangularlayout.test.ts +65 -0
- package/src/tests/shapes/branch.test.tsx +40 -0
- package/src/tests/shapes/circle.test.tsx +47 -0
- package/src/tests/shapes/label.test.tsx +101 -0
- package/src/tests/shapes/rectangle.test.tsx +67 -0
- package/src/tests/shapes/types.ts +1 -0
- package/src/utils.ts +57 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +34 -0
- package/vitetest.config.ts +11 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ImmutableTree } from "../../normalized-tree";
|
|
2
|
+
import { NexusImporter } from "./nexus-importer";
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
function stringToReadableStream(str: string) {
|
|
5
|
+
return new ReadableStream({
|
|
6
|
+
start(controller: {
|
|
7
|
+
enqueue: (arg0: Uint8Array<ArrayBuffer>) => void;
|
|
8
|
+
close: () => void;
|
|
9
|
+
}) {
|
|
10
|
+
// Convert the string to a Uint8Array and enqueue it
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
const chunk = encoder.encode(str);
|
|
13
|
+
controller.enqueue(chunk);
|
|
14
|
+
controller.close();
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("Testing nexus importer", () => {
|
|
20
|
+
it("parse Nexus", async function () {
|
|
21
|
+
const nexusString = `#NEXUS
|
|
22
|
+
Begin trees;
|
|
23
|
+
tree tree1 = (((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2;
|
|
24
|
+
End;`;
|
|
25
|
+
|
|
26
|
+
const nexusStream = stringToReadableStream(nexusString);
|
|
27
|
+
|
|
28
|
+
const trees = new NexusImporter(nexusStream).getTrees();
|
|
29
|
+
let i = 0;
|
|
30
|
+
for await (const tree of trees) {
|
|
31
|
+
expect(tree).toBeInstanceOf(ImmutableTree);
|
|
32
|
+
expect(tree.getExternalNodeCount()).toBe(5);
|
|
33
|
+
expect(tree.getInternalNodeCount()).toBe(4);
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
expect(i).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
it("parse Nexus multiple trees", async function () {
|
|
39
|
+
const nexusString = `#NEXUS
|
|
40
|
+
Begin trees;
|
|
41
|
+
tree tree1 = (((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2;
|
|
42
|
+
tree tree2 = (((Tip7:0.04873764378778993,(Tip8:0.6532000878000133,Tip6:0.1321456687110038):0.042190933211748174):0.6632395745455513,Tip5:0.07721466870767527):0.023945361546555622,Tip0:0.013537352040642556,(Tip3:0.13083726861861736,(((Tip9:0.048159442284509385,Tip4:0.0727658295569952):0.03481552263500266,Tip2:0.0821610870291415):0.13059030569905986,Tip1:0.03707788908419223):0.04920278810640517):0.0011230070183312782);
|
|
43
|
+
End;`;
|
|
44
|
+
|
|
45
|
+
const nexusStream = stringToReadableStream(nexusString);
|
|
46
|
+
|
|
47
|
+
const trees = new NexusImporter(nexusStream).getTrees();
|
|
48
|
+
let i = 0;
|
|
49
|
+
let lastTree;
|
|
50
|
+
for await (const tree of trees) {
|
|
51
|
+
expect(tree).toBeInstanceOf(ImmutableTree);
|
|
52
|
+
lastTree = tree;
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
expect(i).toBe(2);
|
|
56
|
+
expect(lastTree?.getTaxonCount()).toBe(15); //has taxa available from all trees
|
|
57
|
+
});
|
|
58
|
+
it("parse Nexus - treeannotations", async function () {
|
|
59
|
+
const nexusString = `#NEXUS
|
|
60
|
+
Begin trees;
|
|
61
|
+
tree tree1 = (((virus1[&test="A",height=90]:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2;
|
|
62
|
+
End;`;
|
|
63
|
+
|
|
64
|
+
const nexusStream = stringToReadableStream(nexusString);
|
|
65
|
+
|
|
66
|
+
const trees = new NexusImporter(nexusStream).getTrees();
|
|
67
|
+
let i = 0;
|
|
68
|
+
for await (const tree of trees) {
|
|
69
|
+
expect(tree).toBeInstanceOf(ImmutableTree);
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
expect(i).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
it("parse Nexus multiple trees, taxa", async function () {
|
|
75
|
+
const nexusString = `#NEXUS
|
|
76
|
+
Begin taxa;
|
|
77
|
+
Dimensions ntax=5;
|
|
78
|
+
TaxLabels Tip0 Tip1 Tip2 Tip3 Tip4;
|
|
79
|
+
End;
|
|
80
|
+
Begin trees;
|
|
81
|
+
tree 1 = ((Tip3:0.005683055909367637,(Tip4:0.028897909268250865,Tip2:0.17725578701486128):0.15953222892967397):0.0015945469810341964,Tip0:0.16482157194489672,Tip1:0.06558413486573018);
|
|
82
|
+
tree 2 = (Tip3:0.07895643336574805,Tip0:0.09039621608818542,(Tip2:0.12447554684923097,(Tip4:0.016569217859539333,Tip1:0.04529906914098861):0.4270260158852053):0.5512862407921115);
|
|
83
|
+
tree 3 = ((Tip4:0.15573112107230094,Tip2:0.10434839467828654):0.10910110483363722,Tip0:0.009239519001625887,(Tip3:0.4614463596422172,Tip1:0.087294319045952):0.22603324870135144);
|
|
84
|
+
End;`;
|
|
85
|
+
|
|
86
|
+
const nexusStream = stringToReadableStream(nexusString);
|
|
87
|
+
|
|
88
|
+
const trees = new NexusImporter(nexusStream).getTrees();
|
|
89
|
+
let i = 0;
|
|
90
|
+
let lastTree;
|
|
91
|
+
for await (const tree of trees) {
|
|
92
|
+
expect(tree).toBeInstanceOf(ImmutableTree);
|
|
93
|
+
lastTree = tree;
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
expect(i).toBe(3);
|
|
97
|
+
expect(lastTree?.getTaxonCount()).toBe(5); //has taxa available from all trees
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { notNull } from "../../../../utils";
|
|
2
|
+
import type { ImmutableTree } from "../../normalized-tree";
|
|
3
|
+
import { TaxonSet } from "../../taxa/taxon";
|
|
4
|
+
|
|
5
|
+
import { NewickCharacterParser } from "../newick-character-parser";
|
|
6
|
+
import { newickDeliminators, nexusTokenizer } from "./nexus-tokenizer";
|
|
7
|
+
|
|
8
|
+
export class NexusImporter {
|
|
9
|
+
reader: ReadableStreamDefaultReader<string>;
|
|
10
|
+
taxonSet: TaxonSet;
|
|
11
|
+
currentBlock: string | undefined;
|
|
12
|
+
hasTree: boolean | undefined;
|
|
13
|
+
options: { labelName?: string };
|
|
14
|
+
translateTaxonMap: Map<string, string> | undefined;
|
|
15
|
+
constructor(
|
|
16
|
+
stream: ReadableStream<BufferSource>,
|
|
17
|
+
options: { labelName?: string } = {},
|
|
18
|
+
) {
|
|
19
|
+
const tokenizer = new TransformStream(nexusTokenizer());
|
|
20
|
+
|
|
21
|
+
this.reader = stream
|
|
22
|
+
.pipeThrough(new TextDecoderStream())
|
|
23
|
+
.pipeThrough(tokenizer)
|
|
24
|
+
.getReader();
|
|
25
|
+
|
|
26
|
+
this.taxonSet = new TaxonSet();
|
|
27
|
+
this.currentBlock = undefined;
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async *getTrees(): AsyncIterableIterator<ImmutableTree> {
|
|
32
|
+
while (this.currentBlock !== "trees") {
|
|
33
|
+
await this.parseNextBlock();
|
|
34
|
+
this.hasTree = true;
|
|
35
|
+
}
|
|
36
|
+
yield* this.parseTreesBlock();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async parseNextBlock() {
|
|
40
|
+
const blockName = await this.getNextBlockName();
|
|
41
|
+
switch (blockName) {
|
|
42
|
+
case "taxa":
|
|
43
|
+
this.currentBlock = "taxa";
|
|
44
|
+
await this.parseTaxaBlock();
|
|
45
|
+
break;
|
|
46
|
+
case "trees":
|
|
47
|
+
this.currentBlock = "trees";
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
console.log(
|
|
51
|
+
`skipping block ${blockName}. Only parsing blocks are taxa and trees for now.`,
|
|
52
|
+
);
|
|
53
|
+
await this.readToEndOfBlock();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async nextToken() {
|
|
58
|
+
const { done, value } = await this.reader.read();
|
|
59
|
+
if (done) {
|
|
60
|
+
throw new Error("unexpectedly hit the end of the stream");
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
//make skip and then a list of regex to skip
|
|
65
|
+
private async skipSemiColon() {
|
|
66
|
+
const { done, value } = await this.reader.read();
|
|
67
|
+
if (done) {
|
|
68
|
+
throw new Error("unexpectedly hit the end of the stream");
|
|
69
|
+
}
|
|
70
|
+
if (!value.match(/;$/)) {
|
|
71
|
+
throw new Error(`expected ";" got ${value}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async getNextBlockName() {
|
|
76
|
+
let keepGoing = true;
|
|
77
|
+
let token;
|
|
78
|
+
while (keepGoing) {
|
|
79
|
+
const value = await this.nextToken();
|
|
80
|
+
if (value.match(/\bbegin/i)) {
|
|
81
|
+
token = await this.nextToken();
|
|
82
|
+
await this.skipSemiColon();
|
|
83
|
+
keepGoing = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async readToEndOfBlock() {
|
|
90
|
+
let keepGoing = true;
|
|
91
|
+
while (keepGoing) {
|
|
92
|
+
const value = await this.nextToken();
|
|
93
|
+
if (value.match(/\bend;/i)) {
|
|
94
|
+
keepGoing = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// private async getNextCommand(command: RegExp) {
|
|
100
|
+
// let value;
|
|
101
|
+
// let keepGoing = true
|
|
102
|
+
// while (keepGoing) {
|
|
103
|
+
// value = await this.nextToken()
|
|
104
|
+
// if (value === ";") {
|
|
105
|
+
// throw `Hit ; looking for ${command}`
|
|
106
|
+
// }
|
|
107
|
+
// if (command.test(value)) {
|
|
108
|
+
// keepGoing=false
|
|
109
|
+
// }
|
|
110
|
+
// }
|
|
111
|
+
// return value
|
|
112
|
+
// }
|
|
113
|
+
// skip until match and return match
|
|
114
|
+
private async skipUntil(stopper: RegExp): Promise<string> {
|
|
115
|
+
let value;
|
|
116
|
+
let keepGoing = true;
|
|
117
|
+
while (keepGoing) {
|
|
118
|
+
value = await this.nextToken();
|
|
119
|
+
if (stopper.test(value)) {
|
|
120
|
+
keepGoing = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (value == undefined)
|
|
124
|
+
throw new Error(`Internal parsing error: ${stopper.source} not found `);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
// read up to match return everything up to including the match
|
|
128
|
+
private async readUntil(stopper: RegExp) {
|
|
129
|
+
let buffer = "";
|
|
130
|
+
let keepGoing = true;
|
|
131
|
+
while (keepGoing) {
|
|
132
|
+
const value = await this.nextToken();
|
|
133
|
+
if (stopper.test(value)) {
|
|
134
|
+
buffer += value;
|
|
135
|
+
keepGoing = false;
|
|
136
|
+
}
|
|
137
|
+
buffer += value;
|
|
138
|
+
}
|
|
139
|
+
return buffer;
|
|
140
|
+
}
|
|
141
|
+
private async parseTaxaBlock() {
|
|
142
|
+
let ntax;
|
|
143
|
+
let keepGoing = true;
|
|
144
|
+
while (keepGoing) {
|
|
145
|
+
const command = await this.skipUntil(/dimensions|taxlabels|end/i);
|
|
146
|
+
switch (true) {
|
|
147
|
+
case /dimensions/i.test(command): {
|
|
148
|
+
const taxaLine = await this.readUntil(/;/);
|
|
149
|
+
const ntaxa = taxaLine.match(/ntax=(\d+);/);
|
|
150
|
+
if (ntaxa) {
|
|
151
|
+
notNull(ntaxa[1], `No number of taxa found despite matching regex`);
|
|
152
|
+
ntax = parseInt(ntaxa[1]);
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Expected dimension in form of ntax=(\\d+);. Got ${taxaLine}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case /taxlabels/i.test(command): {
|
|
161
|
+
let token = await this.nextToken();
|
|
162
|
+
while (token !== ";") {
|
|
163
|
+
this.taxonSet.addTaxon(token);
|
|
164
|
+
token = await this.nextToken();
|
|
165
|
+
}
|
|
166
|
+
if (ntax) {
|
|
167
|
+
if (ntax != this.taxonSet.getTaxonCount()) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`found ${this.taxonSet.getTaxonCount()} taxa. Expected: ${ntax}}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case /end/i.test(command): {
|
|
176
|
+
if (this.taxonSet.getTaxonCount() === 0) {
|
|
177
|
+
throw new Error("hit end of taxa section but didn't find any taxa");
|
|
178
|
+
}
|
|
179
|
+
this.taxonSet.lockTaxa(); // no more taxa can be added since we parsed a block;
|
|
180
|
+
await this.skipSemiColon();
|
|
181
|
+
keepGoing = false;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Reached impossible code looking for dimensions or taxlabels or end in taxa block "${command}"`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async *parseTreesBlock() {
|
|
193
|
+
let token;
|
|
194
|
+
let keepGoing = true;
|
|
195
|
+
while (keepGoing) {
|
|
196
|
+
const command = await this.skipUntil(/translate|tree|end/i);
|
|
197
|
+
switch (true) {
|
|
198
|
+
case /translate/i.test(command): {
|
|
199
|
+
// all white space removed by tranformStream so will be
|
|
200
|
+
// ['key','taxon,'] but may be ['key','taxon',','] if space tween taxa and ,
|
|
201
|
+
this.translateTaxonMap = new Map();
|
|
202
|
+
let i = 0;
|
|
203
|
+
let key;
|
|
204
|
+
token = await this.nextToken();
|
|
205
|
+
while (token !== ";") {
|
|
206
|
+
if (i % 2 == 0) {
|
|
207
|
+
key = token;
|
|
208
|
+
} else {
|
|
209
|
+
if (token[token.length - 1] === ",") {
|
|
210
|
+
token = token.slice(0, -1);
|
|
211
|
+
}
|
|
212
|
+
// todo get taxa to add here.
|
|
213
|
+
if (this.taxonSet.isFinalized) {
|
|
214
|
+
if (!this.taxonSet.hasTaxon(token)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Taxon ${token} not found in taxa block - but found in translate block`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
//new taxon set so add it;
|
|
221
|
+
this.taxonSet.addTaxon(token);
|
|
222
|
+
}
|
|
223
|
+
// const taxon = this.taxonSet.getTaxonByName(token)
|
|
224
|
+
notNull(
|
|
225
|
+
key,
|
|
226
|
+
"Error parsing nexus. Expected key for taxa but found nothing",
|
|
227
|
+
);
|
|
228
|
+
this.translateTaxonMap.set(key, token);
|
|
229
|
+
}
|
|
230
|
+
token = await this.nextToken();
|
|
231
|
+
while (token === ",") {
|
|
232
|
+
token = await this.nextToken();
|
|
233
|
+
} //incase some white space in there
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
this.taxonSet.lockTaxa();
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case /tree/i.test(command): {
|
|
240
|
+
//parse tree
|
|
241
|
+
// put this in loop so the next call parses the next tree;
|
|
242
|
+
|
|
243
|
+
// first token will be tree id
|
|
244
|
+
// then possible annotation
|
|
245
|
+
// Then =
|
|
246
|
+
// then possible annotations
|
|
247
|
+
// then tree
|
|
248
|
+
//tree id =
|
|
249
|
+
await this.nextToken(); //todo - read to'=' not just next token
|
|
250
|
+
const parser = new NewickCharacterParser(this.taxonSet, {
|
|
251
|
+
translateTaxonNames: this.translateTaxonMap,
|
|
252
|
+
});
|
|
253
|
+
// read to first '(';
|
|
254
|
+
token = await this.skipUntil(/\(/);
|
|
255
|
+
let buffer = token
|
|
256
|
+
.split(newickDeliminators)
|
|
257
|
+
.filter((n) => n.length > 0)
|
|
258
|
+
.reverse();
|
|
259
|
+
while (!parser.isDone()) {
|
|
260
|
+
while (buffer.length > 0) {
|
|
261
|
+
const c = buffer.pop();
|
|
262
|
+
notNull(c, `Unexpectedly hit the end of the buffer`);
|
|
263
|
+
parser.parseCharacter(c);
|
|
264
|
+
}
|
|
265
|
+
if (!parser.isDone()) {
|
|
266
|
+
// get next token
|
|
267
|
+
token = await this.nextToken();
|
|
268
|
+
buffer = token
|
|
269
|
+
.split(newickDeliminators)
|
|
270
|
+
.filter((n) => n.length > 0)
|
|
271
|
+
.reverse();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const tree = parser.getTree();
|
|
275
|
+
yield tree;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case /end/i.test(command):
|
|
279
|
+
await this.skipSemiColon();
|
|
280
|
+
this.hasTree = false;
|
|
281
|
+
// Give up the ghost.
|
|
282
|
+
//TODO read to the end of the file.
|
|
283
|
+
this.reader.releaseLock();
|
|
284
|
+
keepGoing = false;
|
|
285
|
+
break;
|
|
286
|
+
default:
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Reached impossible code in treeblock block "${command}"`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { notNull } from "../../../../utils";
|
|
2
|
+
|
|
3
|
+
export enum STATUS {
|
|
4
|
+
PARSING = "parsing",
|
|
5
|
+
IN_SINGLE_QUOTE = "in single quote",
|
|
6
|
+
IN_DOUBLE_QUOTE = "in double quote",
|
|
7
|
+
IN_COMMENT = "in comment",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function nexusTokenizer() {
|
|
11
|
+
return {
|
|
12
|
+
lastChunk: "",
|
|
13
|
+
status: STATUS.PARSING,
|
|
14
|
+
end: "",
|
|
15
|
+
start() {},
|
|
16
|
+
transform(chunk: string, controller: { enqueue: (arg0: string) => void }) {
|
|
17
|
+
// not really any but we'll see
|
|
18
|
+
const data = this.lastChunk + chunk;
|
|
19
|
+
let buffer = "";
|
|
20
|
+
for (let i = 0; i < data.length; i++) {
|
|
21
|
+
const char = data[i];
|
|
22
|
+
notNull(char, `Internal Error. Hit empty character in array`);
|
|
23
|
+
if (this.status === STATUS.PARSING) {
|
|
24
|
+
// on the look out for quotes and comments
|
|
25
|
+
[this.status, this.end] = getStatusAndEnd(char);
|
|
26
|
+
if (this.status === STATUS.IN_COMMENT) {
|
|
27
|
+
// clear the buffer and send the comment separate
|
|
28
|
+
if (buffer.length > 0) {
|
|
29
|
+
controller.enqueue(buffer); //pass it on
|
|
30
|
+
}
|
|
31
|
+
buffer = "";
|
|
32
|
+
}
|
|
33
|
+
} else if (char === this.end) {
|
|
34
|
+
this.status = STATUS.PARSING;
|
|
35
|
+
this.end = "";
|
|
36
|
+
}
|
|
37
|
+
// if not in quote and hit a space then send it on.
|
|
38
|
+
if (this.status === STATUS.PARSING && /\s|;|\]/.test(char)) {
|
|
39
|
+
if (buffer.length > 0) {
|
|
40
|
+
if (/\]/.test(char)) {
|
|
41
|
+
buffer += char;
|
|
42
|
+
} // close the comment and pass
|
|
43
|
+
controller.enqueue(buffer); //pass it on
|
|
44
|
+
buffer = "";
|
|
45
|
+
}
|
|
46
|
+
if (/;/.test(char)) {
|
|
47
|
+
controller.enqueue(char); // sent the ';'
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
buffer += char;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.lastChunk = buffer;
|
|
54
|
+
},
|
|
55
|
+
flush(controller: { enqueue: (arg0: string) => void }) {
|
|
56
|
+
if (this.lastChunk) {
|
|
57
|
+
controller.enqueue(this.lastChunk);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getStatusAndEnd(char: string): [STATUS, string] {
|
|
64
|
+
if (char === "'") {
|
|
65
|
+
return [STATUS.IN_SINGLE_QUOTE, "'"];
|
|
66
|
+
}
|
|
67
|
+
if (char === '"') {
|
|
68
|
+
return [STATUS.IN_DOUBLE_QUOTE, '"'];
|
|
69
|
+
}
|
|
70
|
+
if (char === "[") {
|
|
71
|
+
return [STATUS.IN_COMMENT, "]"];
|
|
72
|
+
}
|
|
73
|
+
return [STATUS.PARSING, ""];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const newickDeliminators =
|
|
77
|
+
/\s*('[^']+'|"[^"]+"|\[&[^[]+]|,|:|\)|\(|;)\s*/;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { TaxonSet, Tree } from "../..";
|
|
2
|
+
|
|
3
|
+
enum NexusBlock {
|
|
4
|
+
Taxa = "taxa",
|
|
5
|
+
Trees = "trees",
|
|
6
|
+
Other = "other"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
enum STATUS {
|
|
10
|
+
BLOCK_NAME_NEXT,
|
|
11
|
+
SKIP_SEMICOLON,
|
|
12
|
+
PARSING,
|
|
13
|
+
SKIPPING,
|
|
14
|
+
IN_BLOCK,
|
|
15
|
+
SKIP_UNTIL,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function NexusTransformStream(){
|
|
19
|
+
// local parameters
|
|
20
|
+
const taxonSet = new TaxonSet();
|
|
21
|
+
let currentBlock :NexusBlock | undefined= undefined;
|
|
22
|
+
let status = STATUS.PARSING;
|
|
23
|
+
let command = undefined;
|
|
24
|
+
|
|
25
|
+
function processChunk(chunk:string):Tree|undefined{
|
|
26
|
+
if(status === STATUS.SKIP_SEMICOLON){
|
|
27
|
+
if(!chunk.match(/;$/)) {
|
|
28
|
+
throw `expected ";" got ${chunk}`;
|
|
29
|
+
}
|
|
30
|
+
status = STATUS.PARSING;
|
|
31
|
+
return;
|
|
32
|
+
}else if(status === STATUS.SKIPPING){
|
|
33
|
+
if(chunk.match(/\bend;/i)){
|
|
34
|
+
status = STATUS.PARSING;
|
|
35
|
+
currentBlock = undefined;
|
|
36
|
+
return;
|
|
37
|
+
}else{
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
} else if(status === STATUS.BLOCK_NAME_NEXT){
|
|
41
|
+
if(chunk.match(/taxa/i)){
|
|
42
|
+
currentBlock = NexusBlock.Taxa;
|
|
43
|
+
status = STATUS.SKIP_SEMICOLON;
|
|
44
|
+
}else if(chunk.match(/trees/i)){
|
|
45
|
+
currentBlock = NexusBlock.Trees;
|
|
46
|
+
status = STATUS.SKIP_SEMICOLON;
|
|
47
|
+
}else{
|
|
48
|
+
console.log(
|
|
49
|
+
`skipping block ${chunk}. Only parsing blocks are taxa and trees for now.`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
status = STATUS.SKIPPING;
|
|
53
|
+
}else if(chunk.match(/\bbegin/i)){
|
|
54
|
+
status = STATUS.BLOCK_NAME_NEXT;
|
|
55
|
+
return;
|
|
56
|
+
}else if(status === STATUS.PARSING && currentBlock === NexusBlock.Taxa){
|
|
57
|
+
if(command)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
return parseTree(chunk);
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseNextBlock(chunk:string){
|
|
66
|
+
const blockName = getNextBlockName(chunk);
|
|
67
|
+
switch (blockName) {
|
|
68
|
+
case "taxa":
|
|
69
|
+
this.currentBlock = "taxa"
|
|
70
|
+
await this.parseTaxaBlock()
|
|
71
|
+
break
|
|
72
|
+
case "trees":
|
|
73
|
+
this.currentBlock = "trees"
|
|
74
|
+
break
|
|
75
|
+
default:
|
|
76
|
+
console.log(
|
|
77
|
+
`skipping block ${blockName}. Only parsing blocks are taxa and trees for now.`,
|
|
78
|
+
)
|
|
79
|
+
await this.readToEndOfBlock()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getNextBlockName(chunck ) {
|
|
84
|
+
while (true) {
|
|
85
|
+
const value = await this.nextToken()
|
|
86
|
+
|
|
87
|
+
if (value.match(/\bbegin/i)) {
|
|
88
|
+
const token = await this.nextToken()
|
|
89
|
+
this.skipSemiColon()
|
|
90
|
+
return token
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
start(){},
|
|
97
|
+
transform(chunk: string, controller: { enqueue: (arg0: Tree) => void }) {
|
|
98
|
+
|
|
99
|
+
const tree = processChunk(chunk);
|
|
100
|
+
|
|
101
|
+
if(tree){
|
|
102
|
+
controller.enqueue(tree);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
},
|
|
106
|
+
flush(controller: any) { }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Maybe, Undefinable } from "@figtreejs/maybe/maybe";
|
|
2
|
+
import { Nothing, Some, MaybeType } from "@figtreejs/maybe/maybe";
|
|
3
|
+
import type { Taxon, TaxonSetData } from "./taxon";
|
|
4
|
+
|
|
5
|
+
export function maybeGetNameFromIndex(
|
|
6
|
+
data: TaxonSetData,
|
|
7
|
+
id: Maybe<number> | number,
|
|
8
|
+
): Maybe<string> {
|
|
9
|
+
let n: number;
|
|
10
|
+
if (id instanceof Object) {
|
|
11
|
+
if (id.type === MaybeType.Nothing) {
|
|
12
|
+
return id;
|
|
13
|
+
} else {
|
|
14
|
+
n = id.value;
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
n = id;
|
|
18
|
+
}
|
|
19
|
+
const name = data.allNames[n] as Undefinable<string>;
|
|
20
|
+
if (name === undefined) {
|
|
21
|
+
return Nothing();
|
|
22
|
+
}
|
|
23
|
+
return Some(name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function maybeGetTaxonByName(
|
|
27
|
+
data: TaxonSetData,
|
|
28
|
+
id: Maybe<string> | string,
|
|
29
|
+
): Maybe<Taxon> {
|
|
30
|
+
let n: string;
|
|
31
|
+
if (id instanceof Object) {
|
|
32
|
+
if (id.type === MaybeType.Nothing) {
|
|
33
|
+
return id;
|
|
34
|
+
} else {
|
|
35
|
+
n = id.value;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
n = id;
|
|
39
|
+
}
|
|
40
|
+
const taxon = data.byName[n] as Undefinable<Taxon>;
|
|
41
|
+
if (taxon === undefined) {
|
|
42
|
+
return Nothing();
|
|
43
|
+
} else {
|
|
44
|
+
return Some(taxon);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./taxon";
|