react-msaview 4.1.0 → 4.2.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/bundle/index.js +56 -52
- package/dist/components/TextTrack.d.ts +2 -1
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/header/Header.js +6 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderMenuExtra.js +8 -2
- package/dist/components/header/HeaderMenuExtra.js.map +1 -1
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/header/ZoomControls.d.ts +1 -1
- package/dist/components/header/ZoomControls.js +1 -46
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/ZoomMenu.d.ts +6 -0
- package/dist/components/header/ZoomMenu.js +33 -0
- package/dist/components/header/ZoomMenu.js.map +1 -0
- package/dist/components/header/ZoomStar.d.ts +6 -0
- package/dist/components/header/ZoomStar.js +40 -0
- package/dist/components/header/ZoomStar.js.map +1 -0
- package/dist/components/msa/MSACanvasBlock.js +10 -2
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +3 -1
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +7 -6
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/flatToTree.d.ts +17 -0
- package/dist/flatToTree.js +41 -0
- package/dist/flatToTree.js.map +1 -0
- package/dist/model.d.ts +19 -24
- package/dist/model.js +43 -21
- package/dist/model.js.map +1 -1
- package/dist/parseAsn1.d.ts +34 -0
- package/dist/parseAsn1.js +385 -0
- package/dist/parseAsn1.js.map +1 -0
- package/dist/parseAsn1.test.d.ts +1 -0
- package/dist/parseAsn1.test.js +8 -0
- package/dist/parseAsn1.test.js.map +1 -0
- package/dist/parsers/ClustalMSA.d.ts +1 -1
- package/dist/parsers/ClustalMSA.js.map +1 -1
- package/dist/parsers/EmfMSA.d.ts +1 -1
- package/dist/parsers/FastaMSA.d.ts +1 -1
- package/dist/parsers/StockholmMSA.d.ts +1 -1
- package/dist/parsers/StockholmMSA.js.map +1 -1
- package/dist/renderToSvg.d.ts +3 -2
- package/dist/renderToSvg.js.map +1 -1
- package/dist/reparseTree.d.ts +1 -1
- package/dist/reparseTree.js +2 -0
- package/dist/reparseTree.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +6 -20
- package/dist/util.js +19 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/__snapshots__/parseAsn1.test.ts.snap +2400 -0
- package/src/components/TextTrack.tsx +2 -1
- package/src/components/Track.tsx +0 -2
- package/src/components/header/Header.tsx +7 -1
- package/src/components/header/HeaderMenuExtra.tsx +8 -2
- package/src/components/header/MultiAlignmentSelector.tsx +1 -1
- package/src/components/header/ZoomControls.tsx +1 -52
- package/src/components/header/ZoomMenu.tsx +42 -0
- package/src/components/header/ZoomStar.tsx +75 -0
- package/src/components/msa/MSACanvasBlock.tsx +15 -2
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
- package/src/components/msa/renderMSABlock.ts +1 -1
- package/src/components/tree/TreeCanvasBlock.tsx +8 -1
- package/src/components/tree/renderTreeCanvas.ts +13 -7
- package/src/flatToTree.ts +57 -0
- package/src/model.ts +71 -50
- package/src/parseAsn1.test.ts +11 -0
- package/src/parseAsn1.ts +494 -0
- package/src/parsers/ClustalMSA.ts +2 -1
- package/src/parsers/EmfMSA.ts +1 -1
- package/src/parsers/FastaMSA.ts +1 -1
- package/src/parsers/StockholmMSA.ts +4 -1
- package/src/renderToSvg.tsx +6 -4
- package/src/reparseTree.ts +3 -1
- package/src/types.ts +44 -0
- package/src/util.ts +26 -22
- package/src/version.ts +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { parseAsn1 } from './parseAsn1'
|
|
6
|
+
|
|
7
|
+
const r = fs.readFileSync(require.resolve('../test/data/tree.asn'), 'utf8')
|
|
8
|
+
|
|
9
|
+
test('real data file', () => {
|
|
10
|
+
expect(parseAsn1(r)).toMatchSnapshot()
|
|
11
|
+
})
|
package/src/parseAsn1.ts
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NCBI ASN.1 Parser
|
|
3
|
+
* A simple hand-made parser for NCBI ASN.1 format
|
|
4
|
+
* Was too lazy to figure out how ASN.1 actually worked, and use a dedicated
|
|
5
|
+
* ASN.1 parser (you have to generally pre-defined your schema)
|
|
6
|
+
* Written with help of Claude AI
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Define types for our ASN structure
|
|
10
|
+
export interface ASNNode {
|
|
11
|
+
id: number
|
|
12
|
+
parent?: number
|
|
13
|
+
features?: ASNFeature[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ASNFeature {
|
|
17
|
+
featureid: number
|
|
18
|
+
value: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ASNDictEntry {
|
|
22
|
+
id: number
|
|
23
|
+
name: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BioTreeContainer {
|
|
27
|
+
fdict: ASNDictEntry[]
|
|
28
|
+
nodes: ASNNode[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse the fdict section
|
|
32
|
+
const remap = {
|
|
33
|
+
$NODE_COLLAPSED: 'collapsed',
|
|
34
|
+
$NODE_COLOR: 'color',
|
|
35
|
+
$LABEL_BG_COLOR: 'color',
|
|
36
|
+
'seq-id': 'seqId',
|
|
37
|
+
'seq-title': 'seqTitle',
|
|
38
|
+
'align-index': 'alignIndex',
|
|
39
|
+
'accession-nbr': 'accessionNbr',
|
|
40
|
+
'blast-name': 'blastName',
|
|
41
|
+
'common-name': 'commonName',
|
|
42
|
+
'leaf-count': 'leafCount',
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse NCBI ASN.1 format string into a JavaScript object
|
|
46
|
+
* @param asnString The ASN.1 string to parse
|
|
47
|
+
* @returns Parsed BioTreeContainer object
|
|
48
|
+
*/
|
|
49
|
+
export function parseAsn1(
|
|
50
|
+
asnString: string,
|
|
51
|
+
): { id: number; parent: number; name: string }[] {
|
|
52
|
+
const sections = extractSections(
|
|
53
|
+
asnString
|
|
54
|
+
.replace(/\s+/g, ' ')
|
|
55
|
+
.replace(/\s*{\s*/g, '{')
|
|
56
|
+
.replace(/\s*}\s*/g, '}')
|
|
57
|
+
.replace(/\s*,\s*/g, ',')
|
|
58
|
+
.replace(/\s*::\s*=\s*/g, '::=')
|
|
59
|
+
.replace(/^.*?::=/, ''),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const dict = Object.fromEntries(
|
|
63
|
+
parseFdict(sections.fdict!).map(r => [
|
|
64
|
+
r.id,
|
|
65
|
+
remap[r.name as keyof typeof remap] || r.name,
|
|
66
|
+
]),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return parseNodes(sections.nodes!).map(node => {
|
|
70
|
+
const { features, ...rest } = node
|
|
71
|
+
return {
|
|
72
|
+
...rest,
|
|
73
|
+
...Object.fromEntries(
|
|
74
|
+
features!.map(f => [dict[f.featureid], f.value] as const),
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract main sections from the ASN.1 string
|
|
82
|
+
* @param asnString The ASN.1 string without type definition
|
|
83
|
+
* @returns Object with extracted sections
|
|
84
|
+
*/
|
|
85
|
+
function extractSections(asnString: string): Record<string, string> {
|
|
86
|
+
const sections: Record<string, string> = {}
|
|
87
|
+
|
|
88
|
+
// First, let's clean up the string by removing any leading/trailing whitespace
|
|
89
|
+
const cleanedString = asnString.trim()
|
|
90
|
+
|
|
91
|
+
// Remove the outer braces if they exist
|
|
92
|
+
const contentString =
|
|
93
|
+
cleanedString.startsWith('{') && cleanedString.endsWith('}')
|
|
94
|
+
? cleanedString.slice(1, -1).trim()
|
|
95
|
+
: cleanedString
|
|
96
|
+
|
|
97
|
+
// Now we'll manually parse the top-level sections
|
|
98
|
+
let currentPos = 0
|
|
99
|
+
|
|
100
|
+
while (currentPos < contentString.length) {
|
|
101
|
+
// Skip whitespace
|
|
102
|
+
while (
|
|
103
|
+
currentPos < contentString.length &&
|
|
104
|
+
/\s/.test(contentString[currentPos]!)
|
|
105
|
+
) {
|
|
106
|
+
currentPos++
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (currentPos >= contentString.length) {
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Read section name
|
|
114
|
+
const sectionNameStart = currentPos
|
|
115
|
+
while (
|
|
116
|
+
currentPos < contentString.length &&
|
|
117
|
+
/\w/.test(contentString[currentPos]!)
|
|
118
|
+
) {
|
|
119
|
+
currentPos++
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
currentPos >= contentString.length ||
|
|
124
|
+
(contentString[currentPos] !== ' ' && contentString[currentPos] !== '{')
|
|
125
|
+
) {
|
|
126
|
+
// Not a valid section, skip to next comma or end
|
|
127
|
+
while (
|
|
128
|
+
currentPos < contentString.length &&
|
|
129
|
+
contentString[currentPos] !== ','
|
|
130
|
+
) {
|
|
131
|
+
currentPos++
|
|
132
|
+
}
|
|
133
|
+
if (currentPos < contentString.length) {
|
|
134
|
+
currentPos++
|
|
135
|
+
} // Skip the comma
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sectionName = contentString.slice(sectionNameStart, currentPos).trim()
|
|
140
|
+
|
|
141
|
+
// Skip whitespace
|
|
142
|
+
while (
|
|
143
|
+
currentPos < contentString.length &&
|
|
144
|
+
/\s/.test(contentString[currentPos]!)
|
|
145
|
+
) {
|
|
146
|
+
currentPos++
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
currentPos >= contentString.length ||
|
|
151
|
+
contentString[currentPos] !== '{'
|
|
152
|
+
) {
|
|
153
|
+
// Not a valid section, skip to next comma or end
|
|
154
|
+
while (
|
|
155
|
+
currentPos < contentString.length &&
|
|
156
|
+
contentString[currentPos] !== ','
|
|
157
|
+
) {
|
|
158
|
+
currentPos++
|
|
159
|
+
}
|
|
160
|
+
if (currentPos < contentString.length) {
|
|
161
|
+
currentPos++
|
|
162
|
+
} // Skip the comma
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// We found an opening brace, now we need to find the matching closing brace
|
|
167
|
+
const sectionContentStart = currentPos + 1
|
|
168
|
+
let braceCount = 1
|
|
169
|
+
currentPos++
|
|
170
|
+
|
|
171
|
+
while (currentPos < contentString.length && braceCount > 0) {
|
|
172
|
+
if (contentString[currentPos] === '{') {
|
|
173
|
+
braceCount++
|
|
174
|
+
} else if (contentString[currentPos] === '}') {
|
|
175
|
+
braceCount--
|
|
176
|
+
}
|
|
177
|
+
currentPos++
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (braceCount === 0) {
|
|
181
|
+
// We found the matching closing brace
|
|
182
|
+
const sectionContent = contentString
|
|
183
|
+
.slice(sectionContentStart, currentPos - 1)
|
|
184
|
+
.trim()
|
|
185
|
+
sections[sectionName] = sectionContent
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Skip to next comma or end
|
|
189
|
+
while (
|
|
190
|
+
currentPos < contentString.length &&
|
|
191
|
+
contentString[currentPos] !== ','
|
|
192
|
+
) {
|
|
193
|
+
currentPos++
|
|
194
|
+
}
|
|
195
|
+
if (currentPos < contentString.length) {
|
|
196
|
+
currentPos++
|
|
197
|
+
} // Skip the comma
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return sections
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse the fdict section
|
|
205
|
+
* @param fdictString The fdict section content
|
|
206
|
+
* @returns Array of ASNDictEntry objects
|
|
207
|
+
*/
|
|
208
|
+
function parseFdict(fdictString: string): ASNDictEntry[] {
|
|
209
|
+
const entries: ASNDictEntry[] = []
|
|
210
|
+
|
|
211
|
+
// We need to properly handle nested braces
|
|
212
|
+
let currentPos = 0
|
|
213
|
+
|
|
214
|
+
while (currentPos < fdictString.length) {
|
|
215
|
+
// Skip whitespace
|
|
216
|
+
while (
|
|
217
|
+
currentPos < fdictString.length &&
|
|
218
|
+
/\s/.test(fdictString[currentPos]!)
|
|
219
|
+
) {
|
|
220
|
+
currentPos++
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (currentPos >= fdictString.length) {
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Look for opening brace
|
|
228
|
+
if (fdictString[currentPos] === '{') {
|
|
229
|
+
const entryContentStart = currentPos + 1
|
|
230
|
+
let braceCount = 1
|
|
231
|
+
currentPos++
|
|
232
|
+
|
|
233
|
+
while (currentPos < fdictString.length && braceCount > 0) {
|
|
234
|
+
if (fdictString[currentPos] === '{') {
|
|
235
|
+
braceCount++
|
|
236
|
+
} else if (fdictString[currentPos] === '}') {
|
|
237
|
+
braceCount--
|
|
238
|
+
}
|
|
239
|
+
currentPos++
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (braceCount === 0) {
|
|
243
|
+
// We found the matching closing brace
|
|
244
|
+
const entryContent = fdictString
|
|
245
|
+
.slice(entryContentStart, currentPos - 1)
|
|
246
|
+
.trim()
|
|
247
|
+
const entry = parseDictEntry(entryContent)
|
|
248
|
+
if (entry) {
|
|
249
|
+
entries.push(entry)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Skip to next opening brace
|
|
254
|
+
while (
|
|
255
|
+
currentPos < fdictString.length &&
|
|
256
|
+
fdictString[currentPos] !== '{'
|
|
257
|
+
) {
|
|
258
|
+
currentPos++
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return entries
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse a single dictionary entry
|
|
268
|
+
* @param entryString The entry content
|
|
269
|
+
* @returns ASNDictEntry object or null if parsing fails
|
|
270
|
+
*/
|
|
271
|
+
function parseDictEntry(entryString: string): ASNDictEntry | null {
|
|
272
|
+
const idMatch = /id\s+(\d+)/.exec(entryString)
|
|
273
|
+
// Handle escaped quotes in strings
|
|
274
|
+
const nameMatch = /name\s+"((?:[^"\\]|\\.)*)"/s.exec(entryString)
|
|
275
|
+
|
|
276
|
+
if (idMatch && nameMatch) {
|
|
277
|
+
// Process escaped characters in the string
|
|
278
|
+
const processedName = nameMatch[1]!
|
|
279
|
+
.replace(/\\"/g, '"')
|
|
280
|
+
.replace(/\\\\/g, '\\')
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
id: parseInt(idMatch[1]!, 10),
|
|
284
|
+
name: processedName,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parse the nodes section
|
|
293
|
+
* @param nodesString The nodes section content
|
|
294
|
+
* @returns Array of ASNNode objects
|
|
295
|
+
*/
|
|
296
|
+
function parseNodes(nodesString: string): ASNNode[] {
|
|
297
|
+
const nodes: ASNNode[] = []
|
|
298
|
+
|
|
299
|
+
// We need to properly handle nested braces
|
|
300
|
+
let currentPos = 0
|
|
301
|
+
|
|
302
|
+
while (currentPos < nodesString.length) {
|
|
303
|
+
// Skip whitespace
|
|
304
|
+
while (
|
|
305
|
+
currentPos < nodesString.length &&
|
|
306
|
+
/\s/.test(nodesString[currentPos]!)
|
|
307
|
+
) {
|
|
308
|
+
currentPos++
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (currentPos >= nodesString.length) {
|
|
312
|
+
break
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Look for opening brace
|
|
316
|
+
if (nodesString[currentPos] === '{') {
|
|
317
|
+
const nodeContentStart = currentPos + 1
|
|
318
|
+
let braceCount = 1
|
|
319
|
+
currentPos++
|
|
320
|
+
|
|
321
|
+
while (currentPos < nodesString.length && braceCount > 0) {
|
|
322
|
+
if (nodesString[currentPos] === '{') {
|
|
323
|
+
braceCount++
|
|
324
|
+
} else if (nodesString[currentPos] === '}') {
|
|
325
|
+
braceCount--
|
|
326
|
+
}
|
|
327
|
+
currentPos++
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (braceCount === 0) {
|
|
331
|
+
// We found the matching closing brace
|
|
332
|
+
const nodeContent = nodesString
|
|
333
|
+
.slice(nodeContentStart, currentPos - 1)
|
|
334
|
+
.trim()
|
|
335
|
+
const node = parseNode(nodeContent)
|
|
336
|
+
if (node) {
|
|
337
|
+
nodes.push(node)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// Skip to next opening brace
|
|
342
|
+
while (
|
|
343
|
+
currentPos < nodesString.length &&
|
|
344
|
+
nodesString[currentPos] !== '{'
|
|
345
|
+
) {
|
|
346
|
+
currentPos++
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return nodes
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Parse a single node
|
|
356
|
+
* @param nodeString The node content
|
|
357
|
+
* @returns ASNNode object or null if parsing fails
|
|
358
|
+
*/
|
|
359
|
+
function parseNode(nodeString: string): ASNNode | null {
|
|
360
|
+
const idMatch = /id\s+(\d+)/.exec(nodeString)
|
|
361
|
+
const parentMatch = /parent\s+(\d+)/.exec(nodeString)
|
|
362
|
+
|
|
363
|
+
if (idMatch) {
|
|
364
|
+
const node: ASNNode = {
|
|
365
|
+
id: parseInt(idMatch[1]!, 10),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (parentMatch) {
|
|
369
|
+
node.parent = parseInt(parentMatch[1]!, 10)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Extract features if present
|
|
373
|
+
// First find the features section
|
|
374
|
+
const featuresIndex = nodeString.indexOf('features')
|
|
375
|
+
if (featuresIndex !== -1) {
|
|
376
|
+
// Find the opening brace after "features"
|
|
377
|
+
const openBraceIndex = nodeString.indexOf('{', featuresIndex)
|
|
378
|
+
if (openBraceIndex !== -1) {
|
|
379
|
+
// Now find the matching closing brace
|
|
380
|
+
let braceCount = 1
|
|
381
|
+
let closeBraceIndex = openBraceIndex + 1
|
|
382
|
+
|
|
383
|
+
while (closeBraceIndex < nodeString.length && braceCount > 0) {
|
|
384
|
+
if (nodeString[closeBraceIndex] === '{') {
|
|
385
|
+
braceCount++
|
|
386
|
+
} else if (nodeString[closeBraceIndex] === '}') {
|
|
387
|
+
braceCount--
|
|
388
|
+
}
|
|
389
|
+
closeBraceIndex++
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (braceCount === 0) {
|
|
393
|
+
// We found the matching closing brace
|
|
394
|
+
const featuresContent = nodeString
|
|
395
|
+
.slice(openBraceIndex + 1, closeBraceIndex - 1)
|
|
396
|
+
.trim()
|
|
397
|
+
node.features = parseFeatures(featuresContent)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return node
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Parse features section of a node
|
|
410
|
+
* @param featuresString The features section content
|
|
411
|
+
* @returns Array of ASNFeature objects
|
|
412
|
+
*/
|
|
413
|
+
function parseFeatures(featuresString: string): ASNFeature[] {
|
|
414
|
+
const features: ASNFeature[] = []
|
|
415
|
+
|
|
416
|
+
// We need to properly handle nested braces
|
|
417
|
+
let currentPos = 0
|
|
418
|
+
|
|
419
|
+
while (currentPos < featuresString.length) {
|
|
420
|
+
// Skip whitespace
|
|
421
|
+
while (
|
|
422
|
+
currentPos < featuresString.length &&
|
|
423
|
+
/\s/.test(featuresString[currentPos]!)
|
|
424
|
+
) {
|
|
425
|
+
currentPos++
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (currentPos >= featuresString.length) {
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Look for opening brace
|
|
433
|
+
if (featuresString[currentPos] === '{') {
|
|
434
|
+
const featureContentStart = currentPos + 1
|
|
435
|
+
let braceCount = 1
|
|
436
|
+
currentPos++
|
|
437
|
+
|
|
438
|
+
while (currentPos < featuresString.length && braceCount > 0) {
|
|
439
|
+
if (featuresString[currentPos] === '{') {
|
|
440
|
+
braceCount++
|
|
441
|
+
} else if (featuresString[currentPos] === '}') {
|
|
442
|
+
braceCount--
|
|
443
|
+
}
|
|
444
|
+
currentPos++
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (braceCount === 0) {
|
|
448
|
+
// We found the matching closing brace
|
|
449
|
+
const featureContent = featuresString
|
|
450
|
+
.slice(featureContentStart, currentPos - 1)
|
|
451
|
+
.trim()
|
|
452
|
+
const feature = parseFeature(featureContent)
|
|
453
|
+
if (feature) {
|
|
454
|
+
features.push(feature)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
// Skip to next opening brace
|
|
459
|
+
while (
|
|
460
|
+
currentPos < featuresString.length &&
|
|
461
|
+
featuresString[currentPos] !== '{'
|
|
462
|
+
) {
|
|
463
|
+
currentPos++
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return features
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Parse a single feature
|
|
473
|
+
* @param featureString The feature content
|
|
474
|
+
* @returns ASNFeature object or null if parsing fails
|
|
475
|
+
*/
|
|
476
|
+
function parseFeature(featureString: string): ASNFeature | null {
|
|
477
|
+
const featureidMatch = /featureid\s+(\d+)/.exec(featureString)
|
|
478
|
+
// Handle escaped quotes in strings
|
|
479
|
+
const valueMatch = /value\s+"((?:[^"\\]|\\.)*)"/s.exec(featureString)
|
|
480
|
+
|
|
481
|
+
if (featureidMatch && valueMatch) {
|
|
482
|
+
// Process escaped characters in the string
|
|
483
|
+
const processedValue = valueMatch[1]!
|
|
484
|
+
.replace(/\\"/g, '"')
|
|
485
|
+
.replace(/\\\\/g, '\\')
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
featureid: parseInt(featureidMatch[1]!, 10),
|
|
489
|
+
value: processedValue,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return null
|
|
494
|
+
}
|
package/src/parsers/EmfMSA.ts
CHANGED
package/src/parsers/FastaMSA.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import Stockholm from 'stockholm-js'
|
|
2
2
|
|
|
3
3
|
import parseNewick from '../parseNewick'
|
|
4
|
-
import {
|
|
4
|
+
import { generateNodeIds } from '../util'
|
|
5
|
+
|
|
6
|
+
import type { NodeWithIds } from '../types'
|
|
7
|
+
|
|
5
8
|
interface StockholmEntry {
|
|
6
9
|
gf: {
|
|
7
10
|
DE?: string[]
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -13,10 +13,12 @@ import { colorContrast } from './util'
|
|
|
13
13
|
import type { MsaViewModel } from './model'
|
|
14
14
|
import type { Theme } from '@mui/material'
|
|
15
15
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export interface ExportSvgOptions {
|
|
17
|
+
theme: Theme
|
|
18
|
+
includeMinimap?: boolean
|
|
19
|
+
exportType: string
|
|
20
|
+
}
|
|
21
|
+
export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
|
|
20
22
|
await when(() => !!model.dataInitialized)
|
|
21
23
|
const { width, height, scrollX, scrollY } = model
|
|
22
24
|
const { exportType, theme, includeMinimap } = opts
|
package/src/reparseTree.ts
CHANGED
package/src/types.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface Accession {
|
|
2
|
+
accession: string
|
|
3
|
+
name: string
|
|
4
|
+
description: string
|
|
5
|
+
}
|
|
6
|
+
export interface BasicTrackModel {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
associatedRowName?: string
|
|
10
|
+
height: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TextTrackModel extends BasicTrackModel {
|
|
14
|
+
customColorScheme?: Record<string, string>
|
|
15
|
+
data: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ITextTrack {
|
|
19
|
+
ReactComponent: React.FC<any>
|
|
20
|
+
model: TextTrackModel
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type BasicTrack = ITextTrack
|
|
24
|
+
|
|
25
|
+
export interface Node {
|
|
26
|
+
children?: Node[]
|
|
27
|
+
name?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NodeWithIds {
|
|
31
|
+
id: string
|
|
32
|
+
name: string
|
|
33
|
+
children: NodeWithIds[]
|
|
34
|
+
length?: number
|
|
35
|
+
noTree?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NodeWithIdsAndLength {
|
|
39
|
+
id: string
|
|
40
|
+
name: string
|
|
41
|
+
children: NodeWithIdsAndLength[]
|
|
42
|
+
noTree?: boolean
|
|
43
|
+
length: number
|
|
44
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { colord, extend } from 'colord'
|
|
|
2
2
|
import namesPlugin from 'colord/plugins/names'
|
|
3
3
|
import { max } from 'd3-array'
|
|
4
4
|
|
|
5
|
+
import type { Node, NodeWithIds } from './types'
|
|
5
6
|
import type { Theme } from '@mui/material'
|
|
6
7
|
import type { HierarchyNode } from 'd3-hierarchy'
|
|
7
8
|
|
|
@@ -14,28 +15,6 @@ export function transform<T>(
|
|
|
14
15
|
return Object.fromEntries(Object.entries(obj).map(cb))
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
interface Node {
|
|
18
|
-
children?: Node[]
|
|
19
|
-
name?: string
|
|
20
|
-
[key: string]: unknown
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface NodeWithIds {
|
|
24
|
-
id: string
|
|
25
|
-
name: string
|
|
26
|
-
children: NodeWithIds[]
|
|
27
|
-
length?: number
|
|
28
|
-
noTree?: boolean
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface NodeWithIdsAndLength {
|
|
32
|
-
id: string
|
|
33
|
-
name: string
|
|
34
|
-
children: NodeWithIdsAndLength[]
|
|
35
|
-
noTree?: boolean
|
|
36
|
-
length: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
18
|
export function generateNodeIds(
|
|
40
19
|
tree: Node,
|
|
41
20
|
parent = 'node',
|
|
@@ -118,3 +97,28 @@ export function clamp(min: number, num: number, max: number) {
|
|
|
118
97
|
export function len(a: { end: number; start: number }) {
|
|
119
98
|
return a.end - a.start
|
|
120
99
|
}
|
|
100
|
+
|
|
101
|
+
export function localStorageGetItem(item: string) {
|
|
102
|
+
return typeof localStorage !== 'undefined'
|
|
103
|
+
? localStorage.getItem(item)
|
|
104
|
+
: undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function localStorageGetBoolean(key: string, defaultVal: boolean) {
|
|
108
|
+
return Boolean(
|
|
109
|
+
JSON.parse(localStorageGetItem(key) || JSON.stringify(defaultVal)),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function localStorageSetItem(str: string, item: string) {
|
|
114
|
+
if (typeof localStorage !== 'undefined') {
|
|
115
|
+
localStorage.setItem(str, item)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function localStorageSetBoolean(key: string, value: boolean) {
|
|
119
|
+
localStorageSetItem(key, JSON.stringify(value))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isGzip(buf: Uint8Array) {
|
|
123
|
+
return buf[0] === 31 && buf[1] === 139 && buf[2] === 8
|
|
124
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '4.
|
|
1
|
+
export const version = '4.2.0'
|