react-msaview 5.0.7 → 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.
- package/bundle/index.js +25 -25
- package/bundle/index.js.map +1 -1
- package/dist/components/Checkbox2.js +3 -6
- package/dist/components/Checkbox2.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +14 -0
- package/dist/components/MSAViewer.js +34 -0
- package/dist/components/MSAViewer.js.map +1 -0
- package/dist/components/Track.js +5 -24
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/DomainDialog.js +2 -5
- package/dist/components/dialogs/DomainDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +7 -7
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/dialogs/SettingsDialog.js +3 -19
- package/dist/components/dialogs/SettingsDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
- package/dist/components/header/ColorSchemeMenu.js +19 -0
- package/dist/components/header/ColorSchemeMenu.js.map +1 -0
- package/dist/components/header/{ZoomStar.d.ts → FileMenu.d.ts} +2 -2
- package/dist/components/header/FileMenu.js +71 -0
- package/dist/components/header/FileMenu.js.map +1 -0
- package/dist/components/header/Header.js +8 -6
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderMenu.js +3 -145
- package/dist/components/header/HeaderMenu.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.d.ts +6 -0
- package/dist/components/header/MSASettingsMenu.js +36 -0
- package/dist/components/header/MSASettingsMenu.js.map +1 -0
- package/dist/components/header/SettingsMenu.js +1 -21
- package/dist/components/header/SettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.d.ts +6 -0
- package/dist/components/header/TreeSettingsMenu.js +74 -0
- package/dist/components/header/TreeSettingsMenu.js.map +1 -0
- package/dist/components/header/ZoomMenu.js +0 -8
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/header/getDomainsMenu.d.ts +31 -0
- package/dist/components/header/getDomainsMenu.js +75 -0
- package/dist/components/header/getDomainsMenu.js.map +1 -0
- package/dist/components/import/ImportFormExamples.js +21 -19
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +13 -84
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +1 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +2 -4
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +1 -7
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +14 -91
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +5 -16
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +4 -12
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/constants.js.map +1 -1
- package/dist/fetchUtils.d.ts +0 -1
- package/dist/fetchUtils.js +0 -4
- package/dist/fetchUtils.js.map +1 -1
- package/dist/flatToTree.d.ts +0 -5
- package/dist/flatToTree.js +13 -30
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +28 -0
- package/dist/hierarchy.js +164 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/launchInterProScan.d.ts +0 -5
- package/dist/launchInterProScan.js +5 -3
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model/DataModel.d.ts +9 -0
- package/dist/model/DataModel.js +12 -1
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/msaModel.d.ts +3 -0
- package/dist/model/msaModel.js +0 -1
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.d.ts +3 -6
- package/dist/model/treeModel.js +3 -15
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +24 -77
- package/dist/model.js +117 -239
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +38 -629
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.d.ts +0 -12
- package/dist/parseAsn1.js +125 -332
- package/dist/parseAsn1.js.map +1 -1
- package/dist/useWheelScroll.d.ts +8 -0
- package/dist/useWheelScroll.js +93 -0
- package/dist/useWheelScroll.js.map +1 -0
- package/dist/util.d.ts +1 -6
- package/dist/util.js +5 -34
- package/dist/util.js.map +1 -1
- package/dist/vendor/copyToClipboard.d.ts +1 -10
- package/dist/vendor/copyToClipboard.js +14 -109
- package/dist/vendor/copyToClipboard.js.map +1 -1
- package/dist/vendor/fileSaver.d.ts +1 -11
- package/dist/vendor/fileSaver.js +7 -76
- package/dist/vendor/fileSaver.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +10 -13
- package/src/collapseLogic.test.ts +115 -0
- package/src/components/Checkbox2.tsx +9 -18
- package/src/components/MSAViewer.tsx +67 -0
- package/src/components/Track.tsx +10 -26
- package/src/components/dialogs/DomainDialog.tsx +4 -5
- package/src/components/dialogs/InterProScanDialog.tsx +7 -7
- package/src/components/dialogs/SettingsDialog.tsx +0 -37
- package/src/components/header/ColorSchemeMenu.tsx +35 -0
- package/src/components/header/FileMenu.tsx +84 -0
- package/src/components/header/Header.tsx +8 -6
- package/src/components/header/HeaderMenu.tsx +4 -155
- package/src/components/header/MSASettingsMenu.tsx +48 -0
- package/src/components/header/SettingsMenu.tsx +0 -23
- package/src/components/header/TreeSettingsMenu.tsx +96 -0
- package/src/components/header/ZoomMenu.tsx +0 -8
- package/src/components/header/getDomainsMenu.ts +83 -0
- package/src/components/import/ImportFormExamples.tsx +37 -34
- package/src/components/msa/MSACanvas.tsx +21 -91
- package/src/components/msa/MSACanvasBlock.tsx +1 -3
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
- package/src/components/msa/renderMSABlock.ts +2 -5
- package/src/components/msa/renderMSAMouseover.ts +0 -6
- package/src/components/tree/TreeCanvas.tsx +35 -100
- package/src/components/tree/TreeNodeMenu.tsx +5 -14
- package/src/components/tree/renderTreeCanvas.ts +8 -21
- package/src/constants.ts +0 -2
- package/src/fetchUtils.ts +0 -5
- package/src/flatToTree.ts +20 -38
- package/src/hierarchy.test.ts +120 -0
- package/src/hierarchy.ts +220 -0
- package/src/index.ts +2 -0
- package/src/launchInterProScan.ts +4 -3
- package/src/model/DataModel.ts +12 -1
- package/src/model/msaModel.ts +0 -2
- package/src/model/treeModel.ts +2 -18
- package/src/model.ts +179 -278
- package/src/neighborJoining.ts +38 -628
- package/src/parseAsn1.test.ts +4 -1
- package/src/parseAsn1.ts +135 -405
- package/src/useWheelScroll.ts +109 -0
- package/src/util.ts +5 -50
- package/src/vendor/copyToClipboard.ts +14 -122
- package/src/vendor/fileSaver.ts +8 -105
- package/src/version.ts +1 -1
- package/dist/components/dialogs/AddTrackDialog.d.ts +0 -8
- package/dist/components/dialogs/AddTrackDialog.js +0 -30
- package/dist/components/dialogs/AddTrackDialog.js.map +0 -1
- package/dist/components/dialogs/TabPanel.d.ts +0 -6
- package/dist/components/dialogs/TabPanel.js +0 -6
- package/dist/components/dialogs/TabPanel.js.map +0 -1
- package/dist/components/header/ZoomStar.js +0 -40
- package/dist/components/header/ZoomStar.js.map +0 -1
- package/dist/layout.d.ts +0 -26
- package/dist/layout.js +0 -74
- package/dist/layout.js.map +0 -1
- package/dist/reparseTree.d.ts +0 -2
- package/dist/reparseTree.js +0 -15
- package/dist/reparseTree.js.map +0 -1
- package/src/components/dialogs/AddTrackDialog.tsx +0 -85
- package/src/components/dialogs/TabPanel.tsx +0 -19
- package/src/components/header/ZoomStar.tsx +0 -74
- package/src/createPaletteMap.test.ts +0 -57
- package/src/layout.ts +0 -118
- package/src/reparseTree.ts +0 -18
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
124
|
-
(
|
|
138
|
+
pos >= content.length ||
|
|
139
|
+
(content[pos] !== ' ' && content[pos] !== '{')
|
|
125
140
|
) {
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
146
|
+
const name = content.slice(nameStart, pos).trim()
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
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
|
-
|
|
167
|
+
pos++
|
|
178
168
|
}
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
}
|