@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/eslint.config.js +9 -0
  3. package/package.json +76 -0
  4. package/src/@custom-types/normalize-svg-path.d.ts +13 -0
  5. package/src/@custom-types/parse-svg-path.d.ts +8 -0
  6. package/src/@custom-types/svg-path-types.d.ts +37 -0
  7. package/src/bauble-makers/makers.ts +112 -0
  8. package/src/bauble-makers/set-up-baubles.ts +197 -0
  9. package/src/bauble-makers/utils.ts +61 -0
  10. package/src/components/baubles/bauble.tsx +61 -0
  11. package/src/components/baubles/branches.tsx +13 -0
  12. package/src/components/baubles/clades/cartoon.tsx +68 -0
  13. package/src/components/baubles/clades/highlight.tsx +96 -0
  14. package/src/components/baubles/clades/index.ts +1 -0
  15. package/src/components/baubles/clades.tsx +45 -0
  16. package/src/components/baubles/helpers.tsx +62 -0
  17. package/src/components/baubles/index.ts +16 -0
  18. package/src/components/baubles/labels.tsx +38 -0
  19. package/src/components/baubles/nodes.tsx +51 -0
  20. package/src/components/baubles/shapes/branch.tsx +53 -0
  21. package/src/components/baubles/shapes/circle.tsx +64 -0
  22. package/src/components/baubles/shapes/index.ts +9 -0
  23. package/src/components/baubles/shapes/label.tsx +104 -0
  24. package/src/components/baubles/shapes/rectangle.tsx +83 -0
  25. package/src/components/baubles/types.ts +99 -0
  26. package/src/components/decorations/axis/axis-types.ts +123 -0
  27. package/src/components/decorations/axis/axis.tsx +21 -0
  28. package/src/components/decorations/axis/index.ts +2 -0
  29. package/src/components/decorations/axis/polar-axis-bars.tsx +102 -0
  30. package/src/components/decorations/axis/polar-axis.tsx +175 -0
  31. package/src/components/decorations/axis/rectangular-axis-bars.tsx +53 -0
  32. package/src/components/decorations/axis/rectangular-axis.tsx +151 -0
  33. package/src/components/decorations/index.ts +2 -0
  34. package/src/components/decorations/legend/discrete-legend.tsx +93 -0
  35. package/src/components/decorations/legend/index.ts +1 -0
  36. package/src/components/decorations/legend/legend.tsx +1 -0
  37. package/src/components/figtree/figtree-types.ts +69 -0
  38. package/src/components/figtree/figtree.tsx +136 -0
  39. package/src/components/figtree/index.ts +3 -0
  40. package/src/components/hoc/index.ts +7 -0
  41. package/src/components/hoc/with-branch.tsx +148 -0
  42. package/src/components/hoc/with-branches.tsx +54 -0
  43. package/src/components/hoc/with-clades.tsx +47 -0
  44. package/src/components/hoc/with-node.tsx +183 -0
  45. package/src/components/hoc/with-nodes.tsx +45 -0
  46. package/src/components/index.ts +4 -0
  47. package/src/context/aminated-context.ts +3 -0
  48. package/src/context/dimension-context.ts +22 -0
  49. package/src/context/layout-context.ts +20 -0
  50. package/src/context/scale-context.ts +12 -0
  51. package/src/evo/index.ts +1 -0
  52. package/src/evo/tree/index.ts +5 -0
  53. package/src/evo/tree/mcc-tree.ts +0 -0
  54. package/src/evo/tree/normalized-tree/immutable-tree-helpers.ts +136 -0
  55. package/src/evo/tree/normalized-tree/immutable-tree.test.ts +158 -0
  56. package/src/evo/tree/normalized-tree/immutable-tree.ts +1365 -0
  57. package/src/evo/tree/normalized-tree/index.ts +3 -0
  58. package/src/evo/tree/parsers/annotation-parser.ts +276 -0
  59. package/src/evo/tree/parsers/index.ts +3 -0
  60. package/src/evo/tree/parsers/newick-character-parser.ts +246 -0
  61. package/src/evo/tree/parsers/newick-parsing.ts +22 -0
  62. package/src/evo/tree/parsers/nexus-parser.ts +12 -0
  63. package/src/evo/tree/parsers/nexus-parsing.ts +68 -0
  64. package/src/evo/tree/parsers/parsing.test.ts +289 -0
  65. package/src/evo/tree/parsers/stream-reader/index.ts +1 -0
  66. package/src/evo/tree/parsers/stream-reader/newick-importer.txt +395 -0
  67. package/src/evo/tree/parsers/stream-reader/nexus-importer.test.ts +99 -0
  68. package/src/evo/tree/parsers/stream-reader/nexus-importer.ts +293 -0
  69. package/src/evo/tree/parsers/stream-reader/nexus-tokenizer.ts +77 -0
  70. package/src/evo/tree/parsers/stream-reader/nexus-transform-stream.txt +109 -0
  71. package/src/evo/tree/taxa/helper-functions.ts +46 -0
  72. package/src/evo/tree/taxa/index.ts +1 -0
  73. package/src/evo/tree/taxa/taxon.ts +116 -0
  74. package/src/evo/tree/traversals/index.ts +1 -0
  75. package/src/evo/tree/traversals/preorder-traversal.ts +89 -0
  76. package/src/evo/tree/traversals/traversal-types.ts +6 -0
  77. package/src/evo/tree/tree-types.ts +197 -0
  78. package/src/evo/tree/utilities.ts +44 -0
  79. package/src/index.ts +6 -0
  80. package/src/layouts/functional/index.ts +2 -0
  81. package/src/layouts/functional/radial-layout.ts +150 -0
  82. package/src/layouts/functional/rectangular-layout.ts +71 -0
  83. package/src/layouts/index.ts +3 -0
  84. package/src/layouts/layout-interface.ts +90 -0
  85. package/src/layouts/types.ts +32 -0
  86. package/src/path.helpers.ts +81 -0
  87. package/src/store/polar-scale.ts +145 -0
  88. package/src/store/store.ts +144 -0
  89. package/src/tests/baubles/__snapshots__/branch-labels.test.tsx.snap +901 -0
  90. package/src/tests/baubles/__snapshots__/node-labels.test.tsx.snap +1516 -0
  91. package/src/tests/baubles/branch-labels.test.tsx +103 -0
  92. package/src/tests/baubles/label.svg +131 -0
  93. package/src/tests/baubles/node-labels.test.tsx +126 -0
  94. package/src/tests/clades/__snapshots__/cartoon.test.tsx.snap +327 -0
  95. package/src/tests/clades/__snapshots__/highlight.test.tsx.snap +337 -0
  96. package/src/tests/clades/cartoon.test.tsx +65 -0
  97. package/src/tests/clades/highlight.test.tsx +66 -0
  98. package/src/tests/figtree/__snapshots__/figtree.test.tsx.snap +761 -0
  99. package/src/tests/figtree/figtree.test.tsx +123 -0
  100. package/src/tests/figtree/simple.svg +47 -0
  101. package/src/tests/layouts/radiallayout.test.ts +23 -0
  102. package/src/tests/layouts/rectangularlayout.test.ts +65 -0
  103. package/src/tests/shapes/branch.test.tsx +40 -0
  104. package/src/tests/shapes/circle.test.tsx +47 -0
  105. package/src/tests/shapes/label.test.tsx +101 -0
  106. package/src/tests/shapes/rectangle.test.tsx +67 -0
  107. package/src/tests/shapes/types.ts +1 -0
  108. package/src/utils.ts +57 -0
  109. package/tsconfig.json +12 -0
  110. package/vite.config.ts +34 -0
  111. package/vitetest.config.ts +11 -0
@@ -0,0 +1,3 @@
1
+ // export {NormalizedTree} from "./normalizedTree"
2
+ // export * from "./normalizedTree.types"
3
+ export * from "./immutable-tree";
@@ -0,0 +1,276 @@
1
+ import type {
2
+ Annotation,
3
+ MarkovJumpValue,
4
+ RawAnnotationValue,
5
+ ValueOf,
6
+ } from "../tree-types";
7
+ import { BaseAnnotationType } from "../tree-types";
8
+
9
+ /** What the *parser* can emit before classification */
10
+
11
+ export type ClassifiedValue =
12
+ | {
13
+ type: BaseAnnotationType.DISCRETE;
14
+ value: ValueOf<BaseAnnotationType.DISCRETE>;
15
+ }
16
+ | {
17
+ type: BaseAnnotationType.NUMERICAL;
18
+ value: ValueOf<BaseAnnotationType.NUMERICAL>;
19
+ }
20
+ | {
21
+ type: BaseAnnotationType.BOOLEAN;
22
+ value: ValueOf<BaseAnnotationType.BOOLEAN>;
23
+ }
24
+ | {
25
+ type: BaseAnnotationType.NUMERICAL_SET;
26
+ value: ValueOf<BaseAnnotationType.NUMERICAL_SET>;
27
+ }
28
+ | {
29
+ type: BaseAnnotationType.DISCRETE_SET;
30
+ value: ValueOf<BaseAnnotationType.DISCRETE_SET>;
31
+ }
32
+ | {
33
+ type: BaseAnnotationType.MARKOV_JUMPS;
34
+ value: ValueOf<BaseAnnotationType.MARKOV_JUMPS>;
35
+ }
36
+ | {
37
+ type: BaseAnnotationType.DENSITIES;
38
+ value: ValueOf<BaseAnnotationType.DENSITIES>;
39
+ };
40
+
41
+ type ParsedAnnotationRaw = Record<string, RawAnnotationValue>;
42
+
43
+ // Parse the annotation found in a nexus (or newick string - perish the thought!)
44
+ export function parseAnnotation(annotationString: string): ParsedAnnotationRaw {
45
+ const tokens = annotationString
46
+ .split(/\s*('[^']+'|"[^"]+"|;|\(|\)|,|:|=|\[&|\]|\{|\})\s*/)
47
+ .filter((token) => token.length > 0);
48
+ let annotationKeyNext = true;
49
+ let annotationKey: string = "";
50
+ let isAnnotationARange = false;
51
+ let inSubRange = false;
52
+ let subValue: string[] = [];
53
+ let value: RawAnnotationValue | undefined = undefined;
54
+ const annotations: ParsedAnnotationRaw = {};
55
+
56
+ // expect the first token to be a [& and last ]
57
+ if (tokens[0] !== "[&" || tokens[tokens.length - 1] !== "]") {
58
+ throw new Error(
59
+ "expecting a [& at the start and ] at the end of the annotation",
60
+ );
61
+ }
62
+ for (const token of tokens) {
63
+ if (token === "[&") {
64
+ // open BEAST annotation
65
+ annotationKeyNext = true;
66
+ } else if (token === "=") {
67
+ annotationKeyNext = false;
68
+ } else if (token === ",") {
69
+ if (!isAnnotationARange) {
70
+ annotationKeyNext = true;
71
+
72
+ if (value === undefined) throw new Error(`Empty annotation value`);
73
+ //finalize annotation
74
+ annotations[annotationKey] = value;
75
+ } else {
76
+ continue; //to next value in range
77
+ }
78
+ } else if (token === "{") {
79
+ if (isAnnotationARange) {
80
+ inSubRange = true;
81
+ subValue = [];
82
+ } else {
83
+ value = [];
84
+ }
85
+ isAnnotationARange = true;
86
+ } else if (token === "}") {
87
+ if (inSubRange) {
88
+ inSubRange = false;
89
+ // eslint-disable-next-line
90
+ (value as any[])!.push(subValue);
91
+ } else {
92
+ isAnnotationARange = false;
93
+ }
94
+ } else if (token === "]") {
95
+ // close BEAST annotation
96
+
97
+ //finalize annotation
98
+ if (value === undefined) throw new Error(`Empty annotation value`);
99
+ annotations[annotationKey] = value;
100
+ } else {
101
+ // must be annotation
102
+ // remove any quoting and then trim whitespace
103
+ let annotationToken = token;
104
+ if (annotationToken.startsWith('"') || annotationToken.startsWith("'")) {
105
+ annotationToken = annotationToken.slice(1);
106
+ }
107
+ if (annotationToken.endsWith('"') || annotationToken.endsWith("'")) {
108
+ annotationToken = annotationToken.slice(0, -1);
109
+ }
110
+ if (annotationKeyNext) {
111
+ annotationKey = annotationToken.replace(".", "_");
112
+ } else {
113
+ if (isAnnotationARange) {
114
+ if (inSubRange) {
115
+ subValue.push(annotationToken);
116
+ } else {
117
+ // eslint-disable-next-line
118
+ (value as any[]).push(annotationToken);
119
+ }
120
+ } else {
121
+ if (isNaN(annotationToken as unknown as number)) {
122
+ value = annotationToken;
123
+ } else {
124
+ value = parseFloat(annotationToken);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return annotations;
131
+ }
132
+
133
+ export function processAnnotationValue(
134
+ values: RawAnnotationValue,
135
+ ): ClassifiedValue {
136
+ if (Array.isArray(values)) {
137
+ // if is it an array it could be markov jump or array of values
138
+ // is a set of values
139
+ //Array of array?
140
+ if (Array.isArray(values[0])) {
141
+ // an array of arrays is a markov jump
142
+ // This may be a markov jump array
143
+ const tuples = values as string[][];
144
+
145
+ if (
146
+ tuples
147
+ .map((v) => v.length === 3)
148
+ .reduce((acc, curr) => acc && curr, true)
149
+ ) {
150
+ const jumps: MarkovJumpValue[] = tuples.map(
151
+ ([timeStr, source, dest]) => {
152
+ const timeNum = Number(timeStr);
153
+ if (!Number.isFinite(timeNum)) {
154
+ throw new Error(
155
+ `Expected a markov jump annotation but the first entry ${timeStr} could not be make a number`,
156
+ );
157
+ }
158
+ return { time: timeNum, from: source, to: dest };
159
+ },
160
+ );
161
+ return { type: BaseAnnotationType.MARKOV_JUMPS, value: jumps };
162
+ } else {
163
+ throw Error(
164
+ `Markov jump with dimension ${tuples[0].length} detected. Expected 3. ${tuples.map((t) => t.length).join(",")}`,
165
+ );
166
+ }
167
+ }
168
+ // Flat array check types
169
+ const flat = values as Array<string | number>;
170
+
171
+ const allStrings = flat.every((v) => typeof v === "string");
172
+ const allNumbersAfterCoerce = flat.every((v) => Number.isFinite(Number(v)));
173
+
174
+ if (allNumbersAfterCoerce) {
175
+ const nums = flat.map((v) => Number(v));
176
+ return { type: BaseAnnotationType.NUMERICAL_SET, value: nums };
177
+ }
178
+
179
+ if (allStrings) {
180
+ return {
181
+ type: BaseAnnotationType.DISCRETE_SET,
182
+ value: flat.slice(),
183
+ };
184
+ }
185
+
186
+ // coerce to strings
187
+ return {
188
+ type: BaseAnnotationType.DISCRETE_SET,
189
+ value: flat.map(String),
190
+ };
191
+ // densities
192
+ } else if (isPlainObject(values)) {
193
+ // is a set of properties with values
194
+
195
+ const obj = values as Record<string, string | number | boolean>;
196
+ const entries = Object.entries(obj);
197
+
198
+ const allNumbers = entries.every(([, v]) => Number.isFinite(Number(v)));
199
+ const allBooleans = entries.every(([, v]) => typeof v === "boolean");
200
+
201
+ if (allNumbers) {
202
+ const probs: Record<string, number> = {};
203
+ for (const [k, v] of entries) {
204
+ const n = Number(v);
205
+ probs[k] = n;
206
+ }
207
+ return { type: BaseAnnotationType.DENSITIES, value: probs };
208
+ }
209
+
210
+ if (allBooleans) {
211
+ const set = entries
212
+ .filter(([, v]) => v === true)
213
+ .map(([k]) => k)
214
+ .sort();
215
+ return { type: BaseAnnotationType.DISCRETE_SET, value: set };
216
+ }
217
+
218
+ throw new Error(
219
+ "Unsupported object value: expected numeric (probabilities) or boolean map",
220
+ );
221
+ } else {
222
+ if (typeof values === "boolean") {
223
+ return { type: BaseAnnotationType.BOOLEAN, value: values };
224
+ }
225
+
226
+ if (typeof values === "number") {
227
+ return { type: BaseAnnotationType.NUMERICAL, value: values };
228
+ }
229
+
230
+ if (typeof values === "string") {
231
+ // try boolean literal
232
+ const lower = values.toLowerCase();
233
+ if (lower === "true" || lower === "false") {
234
+ return { type: BaseAnnotationType.BOOLEAN, value: lower === "true" };
235
+ }
236
+ // try number
237
+ const n = Number(values);
238
+ if (Number.isFinite(n)) {
239
+ return { type: BaseAnnotationType.NUMERICAL, value: n };
240
+ }
241
+
242
+ // otherwise discrete label
243
+ return { type: BaseAnnotationType.DISCRETE, value: values };
244
+ }
245
+ }
246
+ throw new Error(`Unsupported annotation value: ${String(values)}`);
247
+ }
248
+
249
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
250
+ return typeof v === "object" && v !== null && !Array.isArray(v);
251
+ }
252
+
253
+ export function writeAnnotationValue(a: Annotation): string {
254
+ switch (a.type) {
255
+ case BaseAnnotationType.DISCRETE:
256
+ return a.value;
257
+ case BaseAnnotationType.BOOLEAN:
258
+ return String(a.value);
259
+ case BaseAnnotationType.NUMERICAL:
260
+ return String(a.value);
261
+ case BaseAnnotationType.NUMERICAL_SET:
262
+ return "{" + a.value.map((d) => String(d)).join(", ") + "}";
263
+ case BaseAnnotationType.DISCRETE_SET:
264
+ return "{" + a.value.join(", ") + "}";
265
+ case BaseAnnotationType.MARKOV_JUMPS:
266
+ return (
267
+ "{" +
268
+ a.value.map((d) => `{${String(d.time)},${d.from},${d.to}}`).join(", ") +
269
+ "}"
270
+ );
271
+ case BaseAnnotationType.DENSITIES:
272
+ throw new Error(
273
+ `No defined why to write densities (${a.id}) as a string. \n Please convert keys and values to separate array annotations.`,
274
+ );
275
+ }
276
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./annotation-parser";
2
+ export * from "./newick-parsing";
3
+ export * from "./nexus-parsing";
@@ -0,0 +1,246 @@
1
+ import type { Taxon } from "../taxa/taxon";
2
+ import { TaxonSet } from "../taxa/taxon";
3
+ import type { NodeRef } from "../tree-types";
4
+ import { ImmutableTree } from "../normalized-tree/immutable-tree";
5
+ import { parseAnnotation } from "./annotation-parser";
6
+ import { notNull, unNullify } from "../../../utils";
7
+
8
+ export class NewickCharacterParser {
9
+ done: boolean;
10
+ started: boolean;
11
+ level: number;
12
+ currentNode: NodeRef | undefined;
13
+ nodeStack: NodeRef[];
14
+ labelNext: boolean;
15
+ lengthNext: boolean;
16
+ taxonSet: TaxonSet;
17
+ tree: ImmutableTree;
18
+ options: { labelName?: string; translateTaxonNames?: Map<string, string> };
19
+ constructor(
20
+ taxonSet: TaxonSet = new TaxonSet(),
21
+ options: {
22
+ labelName?: string;
23
+ translateTaxonNames?: Map<string, string>;
24
+ } = {},
25
+ ) {
26
+ this.done = false;
27
+ this.started = false;
28
+ this.level = 0;
29
+ this.currentNode = undefined;
30
+ this.nodeStack = [];
31
+ this.labelNext = false;
32
+ this.lengthNext = false;
33
+ this.taxonSet = taxonSet;
34
+ this.options = options;
35
+ this.tree = new ImmutableTree({ taxonSet: this.taxonSet });
36
+ }
37
+ isDone() {
38
+ return this.done;
39
+ }
40
+ isStarted() {
41
+ return this.started;
42
+ }
43
+ getTree(): ImmutableTree {
44
+ if (!this.done) {
45
+ throw new Error("expecting a semi-colon at the end of the newick string");
46
+ }
47
+ if (!this.started) {
48
+ throw new Error("No tree to give - parsing has not started.");
49
+ }
50
+ //set hights
51
+
52
+ return this.tree;
53
+ }
54
+
55
+ parseCharacter(t: string): void {
56
+ if (this.done) {
57
+ throw new Error("Parsing is done. We have seen a ';'");
58
+ }
59
+ if (t.length > 2 && t.substring(0, 2) === "[&") {
60
+ const annotations = parseAnnotation(t);
61
+
62
+ // for(const annotation of annotations){
63
+ notNull(
64
+ this.currentNode,
65
+ "Internal Parsing error - Current not is not defined",
66
+ );
67
+ this.tree = this.tree.annotateNode(
68
+ this.currentNode,
69
+ annotations,
70
+ ) as ImmutableTree;
71
+ // }
72
+ } else if (t === ";") {
73
+ // check if done.
74
+ //set done.
75
+ if (this.level > 0) {
76
+ throw new Error(
77
+ `unexpected semi-colon in tree did not reach the root yet`,
78
+ );
79
+ }
80
+ if (!this.started) {
81
+ throw new Error(
82
+ `unexpected semi-colon in tree parsing has not started yet`,
83
+ );
84
+ }
85
+ this.done = true;
86
+ } else if (t === "(") {
87
+ // an internal node
88
+ this.started = true;
89
+ if (this.labelNext) {
90
+ // if labelNext is set then the last bracket has just closed
91
+ // so there shouldn't be an open bracket.
92
+ throw new Error("expecting a comma");
93
+ }
94
+ let node;
95
+ this.level += 1;
96
+ if (this.currentNode !== undefined) {
97
+ const added = this.tree.addNodes(1);
98
+ this.tree = added.tree;
99
+ node = added.nodes[0];
100
+ this.nodeStack.push(this.currentNode);
101
+ } else {
102
+ node = this.tree.getRoot();
103
+ }
104
+ this.currentNode = node;
105
+ } else if (t === ",") {
106
+ // another branch in an internal node
107
+
108
+ this.labelNext = false; // labels are optional
109
+ if (this.lengthNext) {
110
+ throw new Error("branch length missing");
111
+ }
112
+
113
+ const parent = this.nodeStack.pop();
114
+ notNull(parent, `Internal Parsing error - node stack unexpectedly empty`);
115
+ notNull(
116
+ this.currentNode,
117
+ "Internal Parsing error - Current not is not defined",
118
+ );
119
+ this.tree = this.tree.addChild(parent, this.currentNode);
120
+ // tree.setParent(currentNode!,parent)
121
+
122
+ this.currentNode = parent;
123
+ } else if (t === ")") {
124
+ if (this.level === 0) {
125
+ throw new Error(
126
+ "the brackets in the newick file are not balanced: too many closed",
127
+ );
128
+ }
129
+ // finished an internal node
130
+
131
+ this.labelNext = false; // labels are optional
132
+ if (this.lengthNext) {
133
+ throw new Error("branch length missing");
134
+ }
135
+
136
+ // the end of an internal node
137
+ const parent = this.nodeStack.pop();
138
+
139
+ notNull(parent, `Internal Parsing error - node stack unexpectedly empty`);
140
+ notNull(
141
+ this.currentNode,
142
+ "Internal Parsing error - Current not is not defined",
143
+ );
144
+ this.tree = this.tree.addChild(parent, this.currentNode);
145
+ // tree.setParent(currentNode!,parent)
146
+
147
+ this.level -= 1;
148
+ this.currentNode = parent;
149
+
150
+ this.labelNext = true;
151
+ } else if (t === ":") {
152
+ this.labelNext = false; // labels are optional
153
+ this.lengthNext = true;
154
+ } else {
155
+ // not any specific token so may be a label, a length, or an external node name
156
+ if (this.lengthNext) {
157
+ notNull(
158
+ this.currentNode,
159
+ "Internal Parsing error - Current not is not defined",
160
+ );
161
+ this.tree = this.tree.setLength(this.currentNode, parseFloat(t));
162
+ this.lengthNext = false;
163
+ } else if (this.labelNext) {
164
+ if (!t.startsWith("#")) {
165
+ let value: number | string = parseFloat(t);
166
+ if (isNaN(value)) {
167
+ value = t;
168
+ }
169
+ if (this.options.labelName) {
170
+ notNull(
171
+ this.currentNode,
172
+ "Internal Parsing error - Current not is not defined",
173
+ );
174
+ this.tree = this.tree.annotateNode(
175
+ this.currentNode,
176
+ this.options.labelName,
177
+ value,
178
+ ) as ImmutableTree;
179
+ } else {
180
+ console.warn(
181
+ `No label name provided to newick parser but found label ${t}. It will be ignored`,
182
+ );
183
+ }
184
+ } else {
185
+ notNull(
186
+ this.currentNode,
187
+ "Internal Parsing error - Current not is not defined",
188
+ );
189
+ this.tree = this.tree.setLabel(this.currentNode, t.slice(1)); //remove the # todo put it back when writing to newick
190
+ }
191
+ this.labelNext = false;
192
+ } else {
193
+ let name = t; // TODO tree needs be a map that's not the ID
194
+
195
+ // remove any quoting and then trim whitespace
196
+ // TODO add to bit that parses taxa block
197
+ if (name.startsWith('"') || name.startsWith("'")) {
198
+ name = name.slice(1);
199
+ }
200
+ if (name.endsWith('"') || name.endsWith("'")) {
201
+ name = name.slice(0, -1);
202
+ }
203
+ name = name.trim();
204
+
205
+ const added = this.tree.addNodes(1);
206
+ this.tree = added.tree;
207
+ const externalNode = added.nodes[0];
208
+ let taxon: Taxon;
209
+ if (this.options.translateTaxonNames) {
210
+ if (this.options.translateTaxonNames.has(name)) {
211
+ name = unNullify(
212
+ this.options.translateTaxonNames.get(name),
213
+ `${name} not found in taxon translation map`,
214
+ );
215
+ } else {
216
+ throw new Error(
217
+ `No mapping found for ${name} in tipNameMap. It's name will not be updated`,
218
+ );
219
+ }
220
+ }
221
+
222
+ if (this.taxonSet.isFinalized) {
223
+ // if set then it will be finalised by now.
224
+
225
+ if (!this.taxonSet.hasTaxon(name)) {
226
+ // hmm trees won't have
227
+ throw new Error(
228
+ `Taxon ${name} not found in taxa - but found in tree`,
229
+ );
230
+ }
231
+ taxon = this.taxonSet.getTaxonByName(name);
232
+ } else {
233
+ this.taxonSet.addTaxon(name);
234
+ taxon = this.taxonSet.getTaxonByName(name);
235
+ }
236
+
237
+ this.tree = this.tree.setTaxon(externalNode, taxon);
238
+
239
+ if (this.currentNode) {
240
+ this.nodeStack.push(this.currentNode);
241
+ }
242
+ this.currentNode = externalNode;
243
+ }
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,22 @@
1
+ import { NewickCharacterParser } from "./newick-character-parser";
2
+ import type { ImmutableTree, newickParsingOptions } from "..";
3
+ import { TaxonSet } from "..";
4
+
5
+ export function parseNewick(
6
+ newick: string,
7
+ options: newickParsingOptions = {},
8
+ ): ImmutableTree {
9
+ const taxonSet = options.taxonSet ? options.taxonSet : new TaxonSet();
10
+ const tokens = newick
11
+ .split(/\s*('[^']+'|"[^"]+"|\[&[^[]+]|,|:|\)|\(|;)\s*/)
12
+ .filter((token) => token.length > 0);
13
+
14
+ const parser = new NewickCharacterParser(taxonSet, options);
15
+
16
+ for (const token of tokens) {
17
+ parser.parseCharacter(token);
18
+ }
19
+ const tree = parser.getTree();
20
+
21
+ return tree;
22
+ }
@@ -0,0 +1,12 @@
1
+ export default class NexusParser {
2
+ // get lock on file
3
+ // only read when asked for tree.
4
+ tokens: string[];
5
+ constructor(contents: string) {
6
+ this.tokens = contents
7
+ .split(
8
+ /\s*(?:\bBegin\s+|\bbegin\s+|\bBEGIN\s+|\bend\s*;|\bEnd\s*;|\bEND\s*;)\s*/,
9
+ )
10
+ .filter((d) => d !== "");
11
+ }
12
+ }
@@ -0,0 +1,68 @@
1
+ /* eslint-disable */
2
+ import { ImmutableTree } from "../normalized-tree/immutable-tree";
3
+ import { parseNewick } from "./newick-parsing";
4
+
5
+ //ONLY parses the first tree
6
+ export function parseNexus(
7
+ _tree: ImmutableTree,
8
+ nexus: string,
9
+ options = {},
10
+ ): ImmutableTree {
11
+ // odd parts ensure we're not in a taxon label
12
+ //TODO make this parsing more robust
13
+ const nexusTokens = nexus
14
+ .split(
15
+ /\s*(?:\bBegin\s+|\bbegin\s+|\bBEGIN\s+|\bend\s*;|\bEnd\s*;|\bEND\s*;)\s*/,
16
+ )
17
+ .filter((d) => d !== "");
18
+ if (nexusTokens.length === 0 || nexusTokens === undefined) {
19
+ throw new Error(
20
+ "No nexus tokens found in string. This may not be a nexus formated tree",
21
+ );
22
+ }
23
+ const firstToken = nexusTokens.shift()!.trim();
24
+ if (firstToken.toLowerCase() !== "#nexus") {
25
+ throw Error("File does not begin with #NEXUS is it a nexus file?");
26
+ }
27
+ for (const section of nexusTokens) {
28
+ const workingSection = section.replace(/^\s+|\s+$/g, "").split(/\n/);
29
+ const sectionTitle = workingSection.shift()!;
30
+ if (sectionTitle.toLowerCase().trim() === "trees;") {
31
+ let inTaxaMap = false;
32
+ const tipNameMap = new Map();
33
+ for (const token of workingSection) {
34
+ if (token.trim().toLowerCase() === "translate") {
35
+ inTaxaMap = true;
36
+ } else {
37
+ if (inTaxaMap) {
38
+ if (token.trim() === ";") {
39
+ inTaxaMap = false;
40
+ } else {
41
+ const taxaData = token
42
+ .trim()
43
+ .replace(",", "")
44
+ .split(/\s*\s\s*/);
45
+ tipNameMap.set(taxaData[0], taxaData[1]);
46
+ }
47
+ } else {
48
+ const treeString = token.substring(token.indexOf("("));
49
+ if (tipNameMap.size > 0) {
50
+ return parseNewick(treeString, {
51
+ parseAnnotations: true,
52
+ ...options,
53
+ tipNameMap: tipNameMap,
54
+ });
55
+ } else {
56
+ return parseNewick(treeString, {
57
+ parseAnnotations: true,
58
+ ...options,
59
+ tipNameMap: tipNameMap,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ throw new Error("No tree section found in nexus file");
68
+ }