react-msaview 5.0.6 → 5.0.13

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 (190) hide show
  1. package/bundle/index.js +106 -106
  2. package/bundle/index.js.LICENSE.txt +1 -1
  3. package/bundle/index.js.map +1 -1
  4. package/dist/components/Checkbox2.js +3 -6
  5. package/dist/components/Checkbox2.js.map +1 -1
  6. package/dist/components/MSAViewer.d.ts +14 -0
  7. package/dist/components/MSAViewer.js +34 -0
  8. package/dist/components/MSAViewer.js.map +1 -0
  9. package/dist/components/Track.d.ts +0 -4
  10. package/dist/components/Track.js +6 -26
  11. package/dist/components/Track.js.map +1 -1
  12. package/dist/components/dialogs/DomainDialog.js +2 -5
  13. package/dist/components/dialogs/DomainDialog.js.map +1 -1
  14. package/dist/components/dialogs/InterProScanDialog.js +7 -7
  15. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  16. package/dist/components/dialogs/SettingsDialog.js +3 -19
  17. package/dist/components/dialogs/SettingsDialog.js.map +1 -1
  18. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  19. package/dist/components/header/ColorSchemeMenu.js +19 -0
  20. package/dist/components/header/ColorSchemeMenu.js.map +1 -0
  21. package/dist/components/header/{ZoomStar.d.ts → FileMenu.d.ts} +2 -2
  22. package/dist/components/header/FileMenu.js +71 -0
  23. package/dist/components/header/FileMenu.js.map +1 -0
  24. package/dist/components/header/Header.js +8 -6
  25. package/dist/components/header/Header.js.map +1 -1
  26. package/dist/components/header/HeaderMenu.js +3 -145
  27. package/dist/components/header/HeaderMenu.js.map +1 -1
  28. package/dist/components/header/MSASettingsMenu.d.ts +6 -0
  29. package/dist/components/header/MSASettingsMenu.js +36 -0
  30. package/dist/components/header/MSASettingsMenu.js.map +1 -0
  31. package/dist/components/header/SettingsMenu.js +1 -21
  32. package/dist/components/header/SettingsMenu.js.map +1 -1
  33. package/dist/components/header/TreeSettingsMenu.d.ts +6 -0
  34. package/dist/components/header/TreeSettingsMenu.js +74 -0
  35. package/dist/components/header/TreeSettingsMenu.js.map +1 -0
  36. package/dist/components/header/ZoomMenu.js +0 -8
  37. package/dist/components/header/ZoomMenu.js.map +1 -1
  38. package/dist/components/header/getDomainsMenu.d.ts +31 -0
  39. package/dist/components/header/getDomainsMenu.js +75 -0
  40. package/dist/components/header/getDomainsMenu.js.map +1 -0
  41. package/dist/components/import/ImportFormExamples.js +21 -19
  42. package/dist/components/import/ImportFormExamples.js.map +1 -1
  43. package/dist/components/msa/MSACanvas.js +13 -89
  44. package/dist/components/msa/MSACanvas.js.map +1 -1
  45. package/dist/components/msa/MSACanvasBlock.js +1 -3
  46. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  47. package/dist/components/msa/renderMSABlock.js +2 -4
  48. package/dist/components/msa/renderMSABlock.js.map +1 -1
  49. package/dist/components/msa/renderMSAMouseover.js +1 -7
  50. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  51. package/dist/components/tree/TreeCanvas.js +18 -101
  52. package/dist/components/tree/TreeCanvas.js.map +1 -1
  53. package/dist/components/tree/TreeCanvasBlock.js +33 -1
  54. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  55. package/dist/components/tree/TreeNodeMenu.js +5 -16
  56. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  57. package/dist/components/tree/renderTreeCanvas.js +4 -12
  58. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  59. package/dist/constants.d.ts +0 -2
  60. package/dist/constants.js +0 -2
  61. package/dist/constants.js.map +1 -1
  62. package/dist/fetchUtils.d.ts +0 -1
  63. package/dist/fetchUtils.js +0 -4
  64. package/dist/fetchUtils.js.map +1 -1
  65. package/dist/flatToTree.d.ts +0 -5
  66. package/dist/flatToTree.js +13 -30
  67. package/dist/flatToTree.js.map +1 -1
  68. package/dist/hierarchy.d.ts +28 -0
  69. package/dist/hierarchy.js +164 -0
  70. package/dist/hierarchy.js.map +1 -0
  71. package/dist/index.d.ts +2 -0
  72. package/dist/index.js +1 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/launchInterProScan.d.ts +0 -5
  75. package/dist/launchInterProScan.js +5 -3
  76. package/dist/launchInterProScan.js.map +1 -1
  77. package/dist/model/DataModel.d.ts +9 -0
  78. package/dist/model/DataModel.js +12 -1
  79. package/dist/model/DataModel.js.map +1 -1
  80. package/dist/model/msaModel.d.ts +3 -0
  81. package/dist/model/msaModel.js +0 -1
  82. package/dist/model/msaModel.js.map +1 -1
  83. package/dist/model/treeModel.d.ts +3 -6
  84. package/dist/model/treeModel.js +3 -15
  85. package/dist/model/treeModel.js.map +1 -1
  86. package/dist/model.d.ts +34 -77
  87. package/dist/model.js +140 -239
  88. package/dist/model.js.map +1 -1
  89. package/dist/neighborJoining.js +40 -633
  90. package/dist/neighborJoining.js.map +1 -1
  91. package/dist/parseAsn1.d.ts +0 -12
  92. package/dist/parseAsn1.js +125 -332
  93. package/dist/parseAsn1.js.map +1 -1
  94. package/dist/useWheelScroll.d.ts +8 -0
  95. package/dist/useWheelScroll.js +93 -0
  96. package/dist/useWheelScroll.js.map +1 -0
  97. package/dist/util.d.ts +1 -6
  98. package/dist/util.js +5 -34
  99. package/dist/util.js.map +1 -1
  100. package/dist/vendor/copyToClipboard.d.ts +1 -10
  101. package/dist/vendor/copyToClipboard.js +14 -109
  102. package/dist/vendor/copyToClipboard.js.map +1 -1
  103. package/dist/vendor/fileSaver.d.ts +1 -11
  104. package/dist/vendor/fileSaver.js +7 -76
  105. package/dist/vendor/fileSaver.js.map +1 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/version.js.map +1 -1
  109. package/package.json +14 -14
  110. package/src/collapseLogic.test.ts +115 -0
  111. package/src/components/Checkbox2.tsx +9 -18
  112. package/src/components/MSAViewer.tsx +67 -0
  113. package/src/components/Track.tsx +11 -30
  114. package/src/components/dialogs/DomainDialog.tsx +4 -5
  115. package/src/components/dialogs/InterProScanDialog.tsx +7 -7
  116. package/src/components/dialogs/SettingsDialog.tsx +0 -37
  117. package/src/components/header/ColorSchemeMenu.tsx +35 -0
  118. package/src/components/header/FileMenu.tsx +84 -0
  119. package/src/components/header/Header.tsx +8 -6
  120. package/src/components/header/HeaderMenu.tsx +4 -155
  121. package/src/components/header/MSASettingsMenu.tsx +48 -0
  122. package/src/components/header/SettingsMenu.tsx +0 -23
  123. package/src/components/header/TreeSettingsMenu.tsx +96 -0
  124. package/src/components/header/ZoomMenu.tsx +0 -8
  125. package/src/components/header/getDomainsMenu.ts +83 -0
  126. package/src/components/import/ImportFormExamples.tsx +37 -34
  127. package/src/components/msa/MSACanvas.tsx +21 -97
  128. package/src/components/msa/MSACanvasBlock.tsx +1 -3
  129. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
  130. package/src/components/msa/renderMSABlock.ts +2 -5
  131. package/src/components/msa/renderMSAMouseover.ts +0 -6
  132. package/src/components/tree/TreeCanvas.tsx +48 -111
  133. package/src/components/tree/TreeCanvasBlock.tsx +44 -0
  134. package/src/components/tree/TreeNodeMenu.tsx +5 -14
  135. package/src/components/tree/renderTreeCanvas.ts +8 -21
  136. package/src/constants.ts +0 -2
  137. package/src/fetchUtils.ts +0 -5
  138. package/src/flatToTree.ts +20 -38
  139. package/src/hierarchy.test.ts +120 -0
  140. package/src/hierarchy.ts +220 -0
  141. package/src/index.ts +2 -0
  142. package/src/launchInterProScan.ts +4 -3
  143. package/src/model/DataModel.ts +12 -1
  144. package/src/model/msaModel.ts +0 -2
  145. package/src/model/treeModel.ts +2 -18
  146. package/src/model.ts +203 -278
  147. package/src/neighborJoining.test.ts +15 -7
  148. package/src/neighborJoining.ts +40 -632
  149. package/src/parseAsn1.test.ts +5 -2
  150. package/src/parseAsn1.ts +135 -405
  151. package/src/useWheelScroll.ts +109 -0
  152. package/src/util.ts +5 -50
  153. package/src/vendor/copyToClipboard.ts +14 -122
  154. package/src/vendor/fileSaver.ts +8 -105
  155. package/src/version.ts +1 -1
  156. package/dist/components/dialogs/AddTrackDialog.d.ts +0 -8
  157. package/dist/components/dialogs/AddTrackDialog.js +0 -30
  158. package/dist/components/dialogs/AddTrackDialog.js.map +0 -1
  159. package/dist/components/dialogs/TabPanel.d.ts +0 -6
  160. package/dist/components/dialogs/TabPanel.js +0 -6
  161. package/dist/components/dialogs/TabPanel.js.map +0 -1
  162. package/dist/components/header/ZoomStar.js +0 -40
  163. package/dist/components/header/ZoomStar.js.map +0 -1
  164. package/dist/createPaletteMap.test.d.ts +0 -1
  165. package/dist/createPaletteMap.test.js +0 -49
  166. package/dist/createPaletteMap.test.js.map +0 -1
  167. package/dist/layout.d.ts +0 -26
  168. package/dist/layout.js +0 -74
  169. package/dist/layout.js.map +0 -1
  170. package/dist/neighborJoining.test.d.ts +0 -1
  171. package/dist/neighborJoining.test.js +0 -110
  172. package/dist/neighborJoining.test.js.map +0 -1
  173. package/dist/parseAsn1.test.d.ts +0 -1
  174. package/dist/parseAsn1.test.js +0 -8
  175. package/dist/parseAsn1.test.js.map +0 -1
  176. package/dist/reparseTree.d.ts +0 -2
  177. package/dist/reparseTree.js +0 -15
  178. package/dist/reparseTree.js.map +0 -1
  179. package/dist/rowCoordinateCalculations.test.d.ts +0 -1
  180. package/dist/rowCoordinateCalculations.test.js +0 -224
  181. package/dist/rowCoordinateCalculations.test.js.map +0 -1
  182. package/dist/seqPosToGlobalCol.test.d.ts +0 -1
  183. package/dist/seqPosToGlobalCol.test.js +0 -60
  184. package/dist/seqPosToGlobalCol.test.js.map +0 -1
  185. package/src/components/dialogs/AddTrackDialog.tsx +0 -85
  186. package/src/components/dialogs/TabPanel.tsx +0 -19
  187. package/src/components/header/ZoomStar.tsx +0 -74
  188. package/src/createPaletteMap.test.ts +0 -57
  189. package/src/layout.ts +0 -118
  190. package/src/reparseTree.ts +0 -18
@@ -1,10 +1,13 @@
1
- import fs from 'fs'
1
+ import { readFileSync } from 'node:fs'
2
2
 
3
3
  import { expect, test } from 'vitest'
4
4
 
5
5
  import { parseAsn1 } from './parseAsn1.ts'
6
6
 
7
- const r = fs.readFileSync(require.resolve('../test/data/tree.asn'), 'utf8')
7
+ const r = readFileSync(
8
+ new URL('../test/data/tree.asn', import.meta.url),
9
+ 'utf8',
10
+ )
8
11
 
9
12
  test('real data file', () => {
10
13
  expect(parseAsn1(r)).toMatchSnapshot()
package/src/parseAsn1.ts CHANGED
@@ -1,12 +1,3 @@
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
1
  export interface ASNNode {
11
2
  id: number
12
3
  parent?: number
@@ -28,8 +19,7 @@ export interface BioTreeContainer {
28
19
  nodes: ASNNode[]
29
20
  }
30
21
 
31
- // Parse the fdict section
32
- const remap = {
22
+ const remap: Record<string, string> = {
33
23
  $NODE_COLLAPSED: 'collapsed',
34
24
  $NODE_COLOR: 'color',
35
25
  $LABEL_BG_COLOR: 'color',
@@ -41,454 +31,194 @@ const remap = {
41
31
  'common-name': 'commonName',
42
32
  'leaf-count': 'leafCount',
43
33
  }
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
- */
34
+
35
+ function extractBracedBlocks(str: string): string[] {
36
+ const blocks: string[] = []
37
+ let pos = 0
38
+ while (pos < str.length) {
39
+ pos = str.indexOf('{', pos)
40
+ if (pos === -1) {
41
+ break
42
+ }
43
+ let depth = 1
44
+ const start = pos + 1
45
+ pos++
46
+ while (pos < str.length && depth > 0) {
47
+ if (str[pos] === '{') {
48
+ depth++
49
+ } else if (str[pos] === '}') {
50
+ depth--
51
+ }
52
+ pos++
53
+ }
54
+ if (depth === 0) {
55
+ blocks.push(str.slice(start, pos - 1).trim())
56
+ }
57
+ }
58
+ return blocks
59
+ }
60
+
61
+ function findBracedContent(str: string, after: number) {
62
+ const openIdx = str.indexOf('{', after)
63
+ if (openIdx === -1) {
64
+ return undefined
65
+ }
66
+ let depth = 1
67
+ let pos = openIdx + 1
68
+ while (pos < str.length && depth > 0) {
69
+ if (str[pos] === '{') {
70
+ depth++
71
+ } else if (str[pos] === '}') {
72
+ depth--
73
+ }
74
+ pos++
75
+ }
76
+ return depth === 0 ? str.slice(openIdx + 1, pos - 1).trim() : undefined
77
+ }
78
+
49
79
  export function parseAsn1(
50
80
  asnString: string,
51
81
  ): { 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
- )
82
+ const normalized = asnString
83
+ .replace(/\s+/g, ' ')
84
+ .replace(/\s*{\s*/g, '{')
85
+ .replace(/\s*}\s*/g, '}')
86
+ .replace(/\s*,\s*/g, ',')
87
+ .replace(/\s*::\s*=\s*/g, '::=')
88
+ .replace(/^.*?::=/, '')
89
+
90
+ const sections = extractSections(normalized)
61
91
 
62
92
  const dict = Object.fromEntries(
63
- parseFdict(sections.fdict!).map(r => [
64
- r.id,
65
- remap[r.name as keyof typeof remap] || r.name,
66
- ]),
93
+ extractBracedBlocks(sections.fdict!).flatMap(block => {
94
+ const entry = parseDictEntry(block)
95
+ return entry ? [[entry.id, remap[entry.name] || entry.name]] : []
96
+ }),
67
97
  )
68
98
 
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
- }
99
+ return extractBracedBlocks(sections.nodes!).flatMap(block => {
100
+ const node = parseNode(block)
101
+ if (!node) {
102
+ return []
103
+ }
104
+ const { features = [], ...rest } = node
105
+ return [
106
+ {
107
+ ...rest,
108
+ ...Object.fromEntries(
109
+ features.map(f => [dict[f.featureid], f.value] as const),
110
+ ),
111
+ },
112
+ ]
77
113
  })
78
114
  }
79
115
 
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
116
  function extractSections(asnString: string): Record<string, string> {
86
117
  const sections: Record<string, string> = {}
118
+ let content = asnString.trim()
119
+ if (content.startsWith('{') && content.endsWith('}')) {
120
+ content = content.slice(1, -1).trim()
121
+ }
87
122
 
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++
123
+ let pos = 0
124
+ while (pos < content.length) {
125
+ while (pos < content.length && /\s/.test(content[pos]!)) {
126
+ pos++
107
127
  }
108
-
109
- if (currentPos >= contentString.length) {
128
+ if (pos >= content.length) {
110
129
  break
111
130
  }
112
131
 
113
- // Read section name
114
- const sectionNameStart = currentPos
115
- while (
116
- currentPos < contentString.length &&
117
- /\w/.test(contentString[currentPos]!)
118
- ) {
119
- currentPos++
132
+ const nameStart = pos
133
+ while (pos < content.length && /\w/.test(content[pos]!)) {
134
+ pos++
120
135
  }
121
136
 
122
137
  if (
123
- currentPos >= contentString.length ||
124
- (contentString[currentPos] !== ' ' && contentString[currentPos] !== '{')
138
+ pos >= content.length ||
139
+ (content[pos] !== ' ' && content[pos] !== '{')
125
140
  ) {
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
141
+ pos = content.indexOf(',', pos)
142
+ pos = pos === -1 ? content.length : pos + 1
136
143
  continue
137
144
  }
138
145
 
139
- const sectionName = contentString.slice(sectionNameStart, currentPos).trim()
146
+ const name = content.slice(nameStart, pos).trim()
140
147
 
141
- // Skip whitespace
142
- while (
143
- currentPos < contentString.length &&
144
- /\s/.test(contentString[currentPos]!)
145
- ) {
146
- currentPos++
148
+ while (pos < content.length && /\s/.test(content[pos]!)) {
149
+ pos++
147
150
  }
148
151
 
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
152
+ if (pos >= content.length || content[pos] !== '{') {
153
+ pos = content.indexOf(',', pos)
154
+ pos = pos === -1 ? content.length : pos + 1
163
155
  continue
164
156
  }
165
157
 
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--
158
+ let depth = 1
159
+ const start = pos + 1
160
+ pos++
161
+ while (pos < content.length && depth > 0) {
162
+ if (content[pos] === '{') {
163
+ depth++
164
+ } else if (content[pos] === '}') {
165
+ depth--
176
166
  }
177
- currentPos++
167
+ pos++
178
168
  }
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
169
+ if (depth === 0) {
170
+ sections[name] = content.slice(start, pos - 1).trim()
186
171
  }
187
172
 
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
173
+ pos = content.indexOf(',', pos)
174
+ pos = pos === -1 ? content.length : pos + 1
198
175
  }
199
176
 
200
177
  return sections
201
178
  }
202
179
 
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
180
  function parseDictEntry(entryString: string): ASNDictEntry | null {
272
181
  const idMatch = /id\s+(\d+)/.exec(entryString)
273
- // Handle escaped quotes in strings
274
182
  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
- }
183
+ if (!idMatch || !nameMatch) {
184
+ return null
286
185
  }
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
- }
186
+ return {
187
+ id: parseInt(idMatch[1]!, 10),
188
+ name: nameMatch[1]!.replace(/\\"/g, '"').replace(/\\\\/g, '\\'),
349
189
  }
350
-
351
- return nodes
352
190
  }
353
191
 
354
- /**
355
- * Parse a single node
356
- * @param nodeString The node content
357
- * @returns ASNNode object or null if parsing fails
358
- */
359
192
  function parseNode(nodeString: string): ASNNode | null {
360
193
  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
194
+ if (!idMatch) {
195
+ return null
403
196
  }
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
- }
197
+ const parentMatch = /parent\s+(\d+)/.exec(nodeString)
198
+ const featuresIdx = nodeString.indexOf('features')
199
+ const featuresContent =
200
+ featuresIdx !== -1 ? findBracedContent(nodeString, featuresIdx) : undefined
201
+
202
+ return {
203
+ id: parseInt(idMatch[1]!, 10),
204
+ ...(parentMatch ? { parent: parseInt(parentMatch[1]!, 10) } : {}),
205
+ features: featuresContent
206
+ ? extractBracedBlocks(featuresContent).flatMap(block => {
207
+ const f = parseFeature(block)
208
+ return f ? [f] : []
209
+ })
210
+ : [],
466
211
  }
467
-
468
- return features
469
212
  }
470
213
 
471
- /**
472
- * Parse a single feature
473
- * @param featureString The feature content
474
- * @returns ASNFeature object or null if parsing fails
475
- */
476
214
  function parseFeature(featureString: string): ASNFeature | null {
477
215
  const featureidMatch = /featureid\s+(\d+)/.exec(featureString)
478
- // Handle escaped quotes in strings
479
216
  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
- }
217
+ if (!featureidMatch || !valueMatch) {
218
+ return null
219
+ }
220
+ return {
221
+ featureid: parseInt(featureidMatch[1]!, 10),
222
+ value: valueMatch[1]!.replace(/\\"/g, '"').replace(/\\\\/g, '\\'),
491
223
  }
492
-
493
- return null
494
224
  }