react-msaview 4.1.1 → 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.
Files changed (75) hide show
  1. package/bundle/index.js +7 -7
  2. package/dist/components/TextTrack.d.ts +2 -1
  3. package/dist/components/TextTrack.js.map +1 -1
  4. package/dist/components/Track.js.map +1 -1
  5. package/dist/components/header/Header.js +6 -1
  6. package/dist/components/header/Header.js.map +1 -1
  7. package/dist/components/header/HeaderMenuExtra.js +8 -2
  8. package/dist/components/header/HeaderMenuExtra.js.map +1 -1
  9. package/dist/components/header/ZoomControls.d.ts +1 -1
  10. package/dist/components/header/ZoomControls.js +1 -46
  11. package/dist/components/header/ZoomControls.js.map +1 -1
  12. package/dist/components/header/ZoomMenu.d.ts +6 -0
  13. package/dist/components/header/ZoomMenu.js +33 -0
  14. package/dist/components/header/ZoomMenu.js.map +1 -0
  15. package/dist/components/header/ZoomStar.d.ts +6 -0
  16. package/dist/components/header/ZoomStar.js +40 -0
  17. package/dist/components/header/ZoomStar.js.map +1 -0
  18. package/dist/components/tree/renderTreeCanvas.js +6 -3
  19. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  20. package/dist/flatToTree.d.ts +17 -0
  21. package/dist/flatToTree.js +41 -0
  22. package/dist/flatToTree.js.map +1 -0
  23. package/dist/model.d.ts +19 -24
  24. package/dist/model.js +42 -20
  25. package/dist/model.js.map +1 -1
  26. package/dist/parseAsn1.d.ts +34 -0
  27. package/dist/parseAsn1.js +385 -0
  28. package/dist/parseAsn1.js.map +1 -0
  29. package/dist/parseAsn1.test.d.ts +1 -0
  30. package/dist/parseAsn1.test.js +8 -0
  31. package/dist/parseAsn1.test.js.map +1 -0
  32. package/dist/parsers/ClustalMSA.d.ts +1 -1
  33. package/dist/parsers/ClustalMSA.js.map +1 -1
  34. package/dist/parsers/EmfMSA.d.ts +1 -1
  35. package/dist/parsers/FastaMSA.d.ts +1 -1
  36. package/dist/parsers/StockholmMSA.d.ts +1 -1
  37. package/dist/parsers/StockholmMSA.js.map +1 -1
  38. package/dist/renderToSvg.d.ts +3 -2
  39. package/dist/renderToSvg.js.map +1 -1
  40. package/dist/reparseTree.d.ts +1 -1
  41. package/dist/reparseTree.js +2 -0
  42. package/dist/reparseTree.js.map +1 -1
  43. package/dist/types.d.ts +38 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/dist/util.d.ts +6 -20
  47. package/dist/util.js +19 -0
  48. package/dist/util.js.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +3 -2
  52. package/src/__snapshots__/parseAsn1.test.ts.snap +2400 -0
  53. package/src/components/TextTrack.tsx +2 -1
  54. package/src/components/Track.tsx +0 -2
  55. package/src/components/header/Header.tsx +7 -1
  56. package/src/components/header/HeaderMenuExtra.tsx +8 -2
  57. package/src/components/header/ZoomControls.tsx +1 -52
  58. package/src/components/header/ZoomMenu.tsx +42 -0
  59. package/src/components/header/ZoomStar.tsx +75 -0
  60. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
  61. package/src/components/msa/renderMSABlock.ts +1 -1
  62. package/src/components/tree/renderTreeCanvas.ts +13 -3
  63. package/src/flatToTree.ts +57 -0
  64. package/src/model.ts +70 -49
  65. package/src/parseAsn1.test.ts +11 -0
  66. package/src/parseAsn1.ts +494 -0
  67. package/src/parsers/ClustalMSA.ts +2 -1
  68. package/src/parsers/EmfMSA.ts +1 -1
  69. package/src/parsers/FastaMSA.ts +1 -1
  70. package/src/parsers/StockholmMSA.ts +4 -1
  71. package/src/renderToSvg.tsx +6 -4
  72. package/src/reparseTree.ts +3 -1
  73. package/src/types.ts +44 -0
  74. package/src/util.ts +26 -22
  75. package/src/version.ts +1 -1
@@ -5,7 +5,8 @@ import { observer } from 'mobx-react'
5
5
 
6
6
  import { colorContrast } from '../util'
7
7
 
8
- import type { ITextTrack, MsaViewModel } from '../model'
8
+ import type { MsaViewModel } from '../model'
9
+ import type { ITextTrack } from '../types'
9
10
 
10
11
  const AnnotationBlock = observer(function ({
11
12
  track,
@@ -6,8 +6,6 @@ import { observer } from 'mobx-react'
6
6
  import normalizeWheel from 'normalize-wheel'
7
7
  import { makeStyles } from 'tss-react/mui'
8
8
 
9
- // icons
10
-
11
9
  import type { MsaViewModel } from '../model'
12
10
 
13
11
  // lazies
@@ -10,6 +10,8 @@ import HeaderMenuExtra from './HeaderMenuExtra'
10
10
  import HeaderStatusArea from './HeaderStatusArea'
11
11
  import MultiAlignmentSelector from './MultiAlignmentSelector'
12
12
  import ZoomControls from './ZoomControls'
13
+ import ZoomMenu from './ZoomMenu'
14
+ import ZoomStar from './ZoomStar'
13
15
 
14
16
  import type { MsaViewModel } from '../../model'
15
17
 
@@ -24,7 +26,11 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
24
26
  <div ref={ref} style={{ display: 'flex' }}>
25
27
  <HeaderMenuExtra model={model} />
26
28
  <ZoomControls model={model} />
27
- <MultiAlignmentSelector model={model} />
29
+ {model.showZoomStar ? <ZoomStar model={model} /> : null}
30
+ <ZoomMenu model={model} />
31
+ <div style={{ margin: 'auto' }}>
32
+ <MultiAlignmentSelector model={model} />
33
+ </div>
28
34
  <HeaderInfoArea model={model} />
29
35
  <Spacer />
30
36
  <HeaderStatusArea model={model} />
@@ -77,7 +77,10 @@ const HeaderMenuExtra = observer(({ model }: { model: MsaViewModel }) => {
77
77
  onClick: () => {
78
78
  model.queueDialog(handleClose => [
79
79
  UserProvidedDomainsDialog,
80
- { handleClose, model },
80
+ {
81
+ handleClose,
82
+ model,
83
+ },
81
84
  ])
82
85
  },
83
86
  },
@@ -118,7 +121,10 @@ const HeaderMenuExtra = observer(({ model }: { model: MsaViewModel }) => {
118
121
  onClick: () => {
119
122
  model.queueDialog(onClose => [
120
123
  FeatureFilterDialog,
121
- { onClose, model },
124
+ {
125
+ onClose,
126
+ model,
127
+ },
122
128
  ])
123
129
  },
124
130
  },
@@ -1,16 +1,11 @@
1
1
  import React from 'react'
2
2
 
3
- import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
- import MoreVert from '@mui/icons-material/MoreVert'
5
- import RestartAlt from '@mui/icons-material/RestartAlt'
6
3
  import ZoomIn from '@mui/icons-material/ZoomIn'
7
4
  import ZoomOut from '@mui/icons-material/ZoomOut'
8
5
  import { IconButton } from '@mui/material'
9
6
  import { observer } from 'mobx-react'
10
7
 
11
- import { MsaViewModel } from '../../model'
12
-
13
- // icons
8
+ import type { MsaViewModel } from '../../model'
14
9
 
15
10
  const ZoomControls = observer(function ZoomControls({
16
11
  model,
@@ -33,52 +28,6 @@ const ZoomControls = observer(function ZoomControls({
33
28
  >
34
29
  <ZoomOut />
35
30
  </IconButton>
36
- <CascadingMenuButton
37
- menuItems={[
38
- {
39
- label: 'Zoom in horizontal',
40
- onClick: () => {
41
- model.zoomInHorizontal()
42
- },
43
- },
44
- {
45
- label: 'Zoom in vertical',
46
- onClick: () => {
47
- model.zoomInVertical()
48
- },
49
- },
50
- {
51
- label: 'Zoom out horizontal',
52
- onClick: () => {
53
- model.zoomOutHorizontal()
54
- },
55
- },
56
-
57
- {
58
- label: 'Zoom out vertical',
59
- onClick: () => {
60
- model.zoomOutVertical()
61
- },
62
- },
63
-
64
- {
65
- label: 'Show entire view',
66
- onClick: () => {
67
- model.showEntire()
68
- },
69
- },
70
- {
71
- label: 'Reset zoom to default',
72
- icon: RestartAlt,
73
- onClick: () => {
74
- model.setColWidth(16)
75
- model.setRowHeight(20)
76
- },
77
- },
78
- ]}
79
- >
80
- <MoreVert />
81
- </CascadingMenuButton>
82
31
  </>
83
32
  )
84
33
  })
@@ -0,0 +1,42 @@
1
+ import React from 'react'
2
+
3
+ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
+ import MoreVert from '@mui/icons-material/MoreVert'
5
+ import RestartAlt from '@mui/icons-material/RestartAlt'
6
+ import { observer } from 'mobx-react'
7
+
8
+ import type { MsaViewModel } from '../../model'
9
+
10
+ const ZoomMenu = observer(function ({ model }: { model: MsaViewModel }) {
11
+ return (
12
+ <CascadingMenuButton
13
+ menuItems={[
14
+ {
15
+ label: 'Fit to view',
16
+ onClick: () => {
17
+ model.showEntire()
18
+ },
19
+ },
20
+ {
21
+ label: 'Reset zoom to default',
22
+ icon: RestartAlt,
23
+ onClick: () => {
24
+ model.resetZoom()
25
+ },
26
+ },
27
+ {
28
+ label: 'Show extra zoom options',
29
+ checked: model.showZoomStar,
30
+ type: 'checkbox',
31
+ onClick: () => {
32
+ model.setShowZoomStar(!model.showZoomStar)
33
+ },
34
+ },
35
+ ]}
36
+ >
37
+ <MoreVert />
38
+ </CascadingMenuButton>
39
+ )
40
+ })
41
+
42
+ export default ZoomMenu
@@ -0,0 +1,75 @@
1
+ import React from 'react'
2
+
3
+ import { IconButton } from '@mui/material'
4
+ import { observer } from 'mobx-react'
5
+ import { makeStyles } from 'tss-react/mui'
6
+
7
+ import { RestartAlt } from '@mui/icons-material'
8
+
9
+ import type { MsaViewModel } from '../../model'
10
+
11
+ const useStyles = makeStyles()(theme => ({
12
+ dpad: {
13
+ display: 'grid',
14
+ gridTemplateColumns: 'repeat(3, 1fr)',
15
+ },
16
+ icon: {
17
+ padding: theme.spacing(0.5),
18
+ },
19
+ }))
20
+
21
+ const ZoomStar = observer(function ({ model }: { model: MsaViewModel }) {
22
+ const { classes } = useStyles()
23
+ return (
24
+ <div className={classes.dpad}>
25
+ <div />
26
+ <IconButton
27
+ className={classes.icon}
28
+ onClick={() => {
29
+ model.zoomInVertical()
30
+ }}
31
+ >
32
+ Y+
33
+ </IconButton>
34
+ <div />
35
+
36
+ <IconButton
37
+ className={classes.icon}
38
+ onClick={() => {
39
+ model.zoomOutHorizontal()
40
+ }}
41
+ >
42
+ X-
43
+ </IconButton>
44
+ <IconButton
45
+ className={classes.icon}
46
+ onClick={() => {
47
+ model.resetZoom()
48
+ }}
49
+ >
50
+ <RestartAlt />
51
+ </IconButton>
52
+ <IconButton
53
+ className={classes.icon}
54
+ onClick={() => {
55
+ model.zoomInHorizontal()
56
+ }}
57
+ >
58
+ X+
59
+ </IconButton>
60
+
61
+ <div />
62
+ <IconButton
63
+ className={classes.icon}
64
+ onClick={() => {
65
+ model.zoomOutVertical()
66
+ }}
67
+ >
68
+ Y-
69
+ </IconButton>
70
+ <div />
71
+ </div>
72
+ )
73
+ })
74
+
75
+ export default ZoomStar
@@ -1,5 +1,5 @@
1
1
  import type { MsaViewModel } from '../../model'
2
- import type { NodeWithIdsAndLength } from '../../util'
2
+ import type { NodeWithIdsAndLength } from '../../types'
3
3
  import type { HierarchyNode } from 'd3-hierarchy'
4
4
 
5
5
  export function renderBoxFeatureCanvasBlock({
@@ -1,7 +1,7 @@
1
1
  import { getClustalXColor, getPercentIdentityColor } from '../../colorSchemes'
2
2
 
3
3
  import type { MsaViewModel } from '../../model'
4
- import type { NodeWithIdsAndLength } from '../../util'
4
+ import type { NodeWithIdsAndLength } from '../../types'
5
5
  import type { Theme } from '@mui/material'
6
6
  import type { HierarchyNode } from 'd3-hierarchy'
7
7
 
@@ -31,9 +31,15 @@ export function renderTree({
31
31
  theme: Theme
32
32
  blockSizeYOverride?: number
33
33
  }) {
34
- const { hierarchy, showBranchLen, blockSize } = model
34
+ const {
35
+ hierarchy,
36
+ allBranchesLength0,
37
+ showBranchLen: showBranchLenPre,
38
+ blockSize,
39
+ } = model
35
40
  const by = blockSizeYOverride || blockSize
36
41
  ctx.strokeStyle = theme.palette.text.primary
42
+ const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
37
43
  for (const link of hierarchy.links()) {
38
44
  const { source, target } = link
39
45
  if (target.height === 0 && !showBranchLen) {
@@ -77,12 +83,14 @@ export function renderNodeBubbles({
77
83
  }) {
78
84
  const {
79
85
  hierarchy,
80
- showBranchLen,
86
+ showBranchLen: showBranchLenPre,
87
+ allBranchesLength0,
81
88
  collapsed,
82
89
  blockSize,
83
90
  marginLeft: ml,
84
91
  } = model
85
92
  const by = blockSizeYOverride || blockSize
93
+ const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
86
94
  for (const node of hierarchy.descendants()) {
87
95
  const val = showBranchLen ? 'len' : 'y'
88
96
  // @ts-expect-error
@@ -131,7 +139,8 @@ export function renderTreeLabels({
131
139
  }) {
132
140
  const {
133
141
  fontSize,
134
- showBranchLen,
142
+ showBranchLen: showBranchLenPre,
143
+ allBranchesLength0,
135
144
  treeMetadata,
136
145
  hierarchy,
137
146
  collapsed,
@@ -153,6 +162,7 @@ export function renderTreeLabels({
153
162
  } else {
154
163
  ctx.textAlign = 'start'
155
164
  }
165
+ const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
156
166
  for (const node of leaves) {
157
167
  const {
158
168
  data: { name, id },
@@ -0,0 +1,57 @@
1
+ // Define the input item interface
2
+ interface FlatItem {
3
+ id: number
4
+ parent?: number
5
+ }
6
+
7
+ // Define the tree node interface
8
+ interface TreeNode {
9
+ id: string
10
+ name: string
11
+ parent?: string
12
+ children: TreeNode[]
13
+ }
14
+
15
+ /**
16
+ * Parses a flat list of items into a tree structure
17
+ * @param items - Array of flat items with id and optional parent
18
+ * @returns Array of root tree nodes
19
+ */
20
+ export function flatToTree(items: FlatItem[]): TreeNode {
21
+ // Create a map to store all nodes by their id for quick lookup
22
+ const nodeMap = new Map<number, TreeNode>()
23
+
24
+ // First pass: Create all tree nodes
25
+ items.forEach(item => {
26
+ nodeMap.set(item.id, {
27
+ ...item,
28
+ id: `${item.id}`,
29
+ name: `${item.id}`,
30
+ parent: item.parent !== undefined ? `${item.parent}` : undefined,
31
+ children: [],
32
+ })
33
+ })
34
+
35
+ // Second pass: Build parent-child relationships
36
+ const roots: TreeNode[] = []
37
+
38
+ items.forEach(item => {
39
+ const node = nodeMap.get(item.id)!
40
+
41
+ if (item.parent !== undefined) {
42
+ // This item has a parent, add it to parent's children
43
+ const parentNode = nodeMap.get(item.parent)
44
+ if (parentNode) {
45
+ parentNode.children.push(node)
46
+ } else {
47
+ // Parent doesn't exist, treat as root
48
+ roots.push(node)
49
+ }
50
+ } else {
51
+ // This item has no parent, it's a root node
52
+ roots.push(node)
53
+ }
54
+ })
55
+
56
+ return roots[0]!
57
+ }
package/src/model.ts CHANGED
@@ -1,11 +1,10 @@
1
- import React from 'react'
2
-
3
1
  import { groupBy, notEmpty, sum } from '@jbrowse/core/util'
4
2
  import { openLocation } from '@jbrowse/core/util/io'
5
3
  import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst'
6
4
  import { colord } from 'colord'
7
5
  import { ascending } from 'd3-array'
8
6
  import { cluster, hierarchy } from 'd3-hierarchy'
7
+ import { parseEmfTree } from 'emf-js'
9
8
  import { saveAs } from 'file-saver'
10
9
  import { autorun, transaction } from 'mobx'
11
10
  import { addDisposer, cast, types } from 'mobx-state-tree'
@@ -15,16 +14,17 @@ import Stockholm from 'stockholm-js'
15
14
  import { blocksX, blocksY } from './calculateBlocks'
16
15
  import colorSchemes from './colorSchemes'
17
16
  import TextTrack from './components/TextTrack'
17
+ import { flatToTree } from './flatToTree'
18
18
  import palettes from './ggplotPalettes'
19
19
  import { measureTextCanvas } from './measureTextCanvas'
20
20
  import { DataModelF } from './model/DataModel'
21
21
  import { DialogQueueSessionMixin } from './model/DialogQueue'
22
22
  import { MSAModelF } from './model/msaModel'
23
23
  import { TreeModelF } from './model/treeModel'
24
+ import { parseAsn1 } from './parseAsn1'
24
25
  import parseNewick from './parseNewick'
25
26
  import ClustalMSA from './parsers/ClustalMSA'
26
27
  import EmfMSA from './parsers/EmfMSA'
27
- import EmfTree from './parsers/EmfTree'
28
28
  import FastaMSA from './parsers/FastaMSA'
29
29
  import StockholmMSA from './parsers/StockholmMSA'
30
30
  import { reparseTree } from './reparseTree'
@@ -36,46 +36,31 @@ import {
36
36
  clamp,
37
37
  collapse,
38
38
  generateNodeIds,
39
+ isGzip,
39
40
  len,
41
+ localStorageGetBoolean,
42
+ localStorageSetBoolean,
40
43
  maxLength,
41
44
  setBrLength,
42
45
  skipBlanks,
43
46
  } from './util'
44
47
 
45
48
  import type { InterProScanResults } from './launchInterProScan'
46
- import type { NodeWithIds, NodeWithIdsAndLength } from './util'
49
+ import type {
50
+ Accession,
51
+ BasicTrack,
52
+ NodeWithIds,
53
+ NodeWithIdsAndLength,
54
+ TextTrackModel,
55
+ } from './types'
47
56
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
48
57
  import type { Theme } from '@mui/material'
49
58
  import type { HierarchyNode } from 'd3-hierarchy'
50
59
  import type { Instance } from 'mobx-state-tree'
51
60
 
52
- export interface Accession {
53
- accession: string
54
- name: string
55
- description: string
56
- }
57
- export interface BasicTrackModel {
58
- id: string
59
- name: string
60
- associatedRowName?: string
61
- height: number
62
- }
63
-
64
- export interface TextTrackModel extends BasicTrackModel {
65
- customColorScheme?: Record<string, string>
66
- data: string
67
- }
68
-
69
- export interface ITextTrack {
70
- ReactComponent: React.FC<any>
71
- model: TextTrackModel
72
- }
73
-
74
- export type BasicTrack = ITextTrack
75
-
76
- export function isGzip(buf: Uint8Array) {
77
- return buf[0] === 31 && buf[1] === 139 && buf[2] === 8
78
- }
61
+ const defaultRowHeight = 16
62
+ const defaultColWidth = 12
63
+ const showZoomStarKey = 'msa-showZoomStar'
79
64
 
80
65
  /**
81
66
  * #stateModel MsaView
@@ -145,7 +130,7 @@ function stateModelFactory() {
145
130
  * #property
146
131
  * height of each row, px
147
132
  */
148
- rowHeight: 18,
133
+ rowHeight: defaultRowHeight,
149
134
 
150
135
  /**
151
136
  * #property
@@ -163,7 +148,7 @@ function stateModelFactory() {
163
148
  * #property
164
149
  * width of columns, px
165
150
  */
166
- colWidth: 14,
151
+ colWidth: defaultColWidth,
167
152
 
168
153
  /**
169
154
  * #property
@@ -250,6 +235,12 @@ function stateModelFactory() {
250
235
  * screens
251
236
  */
252
237
  highResScaleFactor: 2,
238
+
239
+ /**
240
+ * #volatile
241
+ * obtained from localStorage
242
+ */
243
+ showZoomStar: localStorageGetBoolean(showZoomStarKey, true),
253
244
  /**
254
245
  * #volatile
255
246
  */
@@ -363,6 +354,12 @@ function stateModelFactory() {
363
354
  setLoadingMSA(arg: boolean) {
364
355
  self.loadingMSA = arg
365
356
  },
357
+ /**
358
+ * #volatile
359
+ */
360
+ setShowZoomStar(arg: boolean) {
361
+ self.showZoomStar = arg
362
+ },
366
363
  /**
367
364
  * #action
368
365
  */
@@ -635,26 +632,28 @@ function stateModelFactory() {
635
632
  */
636
633
  get tree(): NodeWithIds {
637
634
  const text = self.data.tree
638
- let ret: NodeWithIds
639
635
  if (text) {
640
- let t: string
641
- if (text.startsWith('SEQ')) {
642
- const r = new EmfTree(text)
643
- t = r.data.tree
644
- } else {
645
- t = text
646
- }
647
- ret = generateNodeIds(parseNewick(t))
636
+ return reparseTree(
637
+ generateNodeIds(
638
+ text.startsWith('BioTreeContainer')
639
+ ? flatToTree(parseAsn1(text))
640
+ : parseNewick(
641
+ text.startsWith('SEQ') ? parseEmfTree(text).tree : text,
642
+ ),
643
+ ),
644
+ )
648
645
  } else {
649
- ret = this.MSA?.getTree() || {
650
- noTree: true,
651
- children: [],
652
- id: 'empty',
653
- name: 'empty',
654
- }
646
+ return reparseTree(
647
+ this.MSA?.getTree() || {
648
+ noTree: true,
649
+ children: [],
650
+ id: 'empty',
651
+ name: 'empty',
652
+ },
653
+ )
655
654
  }
656
- return reparseTree(ret)
657
655
  },
656
+
658
657
  /**
659
658
  * #getter
660
659
  */
@@ -846,6 +845,13 @@ function stateModelFactory() {
846
845
  get leaves() {
847
846
  return this.hierarchy.leaves()
848
847
  },
848
+
849
+ /**
850
+ * #getter
851
+ */
852
+ get allBranchesLength0() {
853
+ return this.hierarchy.links().every(s => !s.source.data.length)
854
+ },
849
855
  }))
850
856
  .views(self => ({
851
857
  /**
@@ -937,6 +943,13 @@ function stateModelFactory() {
937
943
  self.drawMsaLetters = arg
938
944
  },
939
945
 
946
+ /**
947
+ * #action
948
+ */
949
+ resetZoom() {
950
+ self.setColWidth(defaultColWidth)
951
+ self.setRowHeight(defaultRowHeight)
952
+ },
940
953
  /**
941
954
  * #action
942
955
  */
@@ -1379,6 +1392,14 @@ function stateModelFactory() {
1379
1392
  }),
1380
1393
  )
1381
1394
 
1395
+ // autorun saves local settings
1396
+ addDisposer(
1397
+ self,
1398
+ autorun(() => {
1399
+ localStorageSetBoolean(showZoomStarKey, self.showZoomStar)
1400
+ }),
1401
+ )
1402
+
1382
1403
  // autorun opens treeFilehandle
1383
1404
  addDisposer(
1384
1405
  self,
@@ -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
+ })