smiles-js 2.0.3 → 2.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/API.md +162 -0
- package/README.md +39 -0
- package/docs/MIRROR_PLAN.md +204 -0
- package/docs/smiles.peggy +215 -0
- package/package.json +1 -1
- package/scripts/coverage-summary.js +1 -1
- package/src/codegen/branch-crossing-ring.js +27 -6
- package/src/codegen/interleaved-fused-ring.js +24 -0
- package/src/decompiler.js +236 -51
- package/src/decompiler.test.js +232 -60
- package/src/fragment.test.js +7 -2
- package/src/manipulation.js +409 -4
- package/src/manipulation.test.js +359 -1
- package/src/method-attachers.js +37 -8
- package/src/node-creators.js +7 -0
- package/src/parser/ast-builder.js +23 -8
- package/src/parser/ring-group-builder.js +14 -2
- package/src/parser/ring-utils.js +28 -0
- package/test-integration/__snapshots__/acetaminophen.test.js.snap +20 -0
- package/test-integration/__snapshots__/adjuvant-analgesics.test.js.snap +63 -1
- package/test-integration/__snapshots__/cholesterol-drugs.test.js.snap +437 -0
- package/test-integration/__snapshots__/dexamethasone.test.js.snap +31 -0
- package/test-integration/__snapshots__/endocannabinoids.test.js.snap +79 -2
- package/test-integration/__snapshots__/endogenous-opioids.test.js.snap +1116 -0
- package/test-integration/__snapshots__/hypertension-medication.test.js.snap +70 -1
- package/test-integration/__snapshots__/local-anesthetics.test.js.snap +97 -0
- package/test-integration/__snapshots__/nsaids-otc.test.js.snap +61 -1
- package/test-integration/__snapshots__/nsaids-prescription.test.js.snap +115 -2
- package/test-integration/__snapshots__/opioids.test.js.snap +113 -4
- package/test-integration/__snapshots__/steroids.test.js.snap +381 -2
- package/test-integration/acetaminophen.test.js +15 -3
- package/test-integration/adjuvant-analgesics.test.js +43 -7
- package/test-integration/cholesterol-drugs.test.js +127 -20
- package/test-integration/cholesterol.test.js +112 -0
- package/test-integration/dexamethasone.test.js +8 -2
- package/test-integration/endocannabinoids.test.js +48 -12
- package/test-integration/endogenous-opioids.smiles.js +32 -0
- package/test-integration/endogenous-opioids.test.js +192 -0
- package/test-integration/hypertension-medication.test.js +32 -8
- package/test-integration/local-anesthetics.smiles.js +33 -0
- package/test-integration/local-anesthetics.test.js +64 -16
- package/test-integration/mirror.test.js +151 -0
- package/test-integration/nsaids-otc.test.js +40 -10
- package/test-integration/nsaids-prescription.test.js +72 -18
- package/test-integration/opioids.test.js +56 -14
- package/test-integration/polymer.test.js +148 -0
- package/test-integration/steroids.test.js +112 -28
- package/test-integration/utils.js +4 -2
- package/todo +2 -3
|
@@ -23,6 +23,12 @@ describe('Gabapentin Integration Test', () => {
|
|
|
23
23
|
expect(code).toMatchSnapshot();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test('generates valid verbose code via toCode()', () => {
|
|
27
|
+
const ast = parse(GABAPENTIN_SMILES);
|
|
28
|
+
const code = ast.toCode('v', { verbose: true });
|
|
29
|
+
expect(code).toMatchSnapshot();
|
|
30
|
+
});
|
|
31
|
+
|
|
26
32
|
test('generated code is valid JavaScript', () => {
|
|
27
33
|
const ast = parse(GABAPENTIN_SMILES);
|
|
28
34
|
const code = ast.toCode('v');
|
|
@@ -30,14 +36,14 @@ describe('Gabapentin Integration Test', () => {
|
|
|
30
36
|
|
|
31
37
|
let factory;
|
|
32
38
|
expect(() => {
|
|
33
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
39
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
34
40
|
}).not.toThrow();
|
|
35
41
|
expect(typeof factory).toBe('function');
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
39
45
|
const ast = parse(GABAPENTIN_SMILES);
|
|
40
|
-
const code = ast.toCode('v');
|
|
46
|
+
const code = ast.toCode('v', { verbose: true });
|
|
41
47
|
const executableCode = stripExports(code);
|
|
42
48
|
|
|
43
49
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -64,9 +70,15 @@ describe('Pregabalin Integration Test', () => {
|
|
|
64
70
|
expect(code).toMatchSnapshot();
|
|
65
71
|
});
|
|
66
72
|
|
|
73
|
+
test('generates valid verbose code via toCode()', () => {
|
|
74
|
+
const ast = parse(PREGABALIN_SMILES);
|
|
75
|
+
const code = ast.toCode('v', { verbose: true });
|
|
76
|
+
expect(code).toMatchSnapshot();
|
|
77
|
+
});
|
|
78
|
+
|
|
67
79
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
68
80
|
const ast = parse(PREGABALIN_SMILES);
|
|
69
|
-
const code = ast.toCode('v');
|
|
81
|
+
const code = ast.toCode('v', { verbose: true });
|
|
70
82
|
const executableCode = stripExports(code);
|
|
71
83
|
|
|
72
84
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -92,9 +104,15 @@ describe('Amitriptyline Integration Test', () => {
|
|
|
92
104
|
expect(code).toMatchSnapshot();
|
|
93
105
|
});
|
|
94
106
|
|
|
107
|
+
test('generates valid verbose code via toCode()', () => {
|
|
108
|
+
const ast = parse(AMITRIPTYLINE_SMILES);
|
|
109
|
+
const code = ast.toCode('v', { verbose: true });
|
|
110
|
+
expect(code).toMatchSnapshot();
|
|
111
|
+
});
|
|
112
|
+
|
|
95
113
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
96
114
|
const ast = parse(AMITRIPTYLINE_SMILES);
|
|
97
|
-
const code = ast.toCode('v');
|
|
115
|
+
const code = ast.toCode('v', { verbose: true });
|
|
98
116
|
const executableCode = stripExports(code);
|
|
99
117
|
|
|
100
118
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -121,9 +139,15 @@ describe('Duloxetine Integration Test', () => {
|
|
|
121
139
|
expect(code).toMatchSnapshot();
|
|
122
140
|
});
|
|
123
141
|
|
|
142
|
+
test('generates valid verbose code via toCode()', () => {
|
|
143
|
+
const ast = parse(DULOXETINE_SMILES);
|
|
144
|
+
const code = ast.toCode('v', { verbose: true });
|
|
145
|
+
expect(code).toMatchSnapshot();
|
|
146
|
+
});
|
|
147
|
+
|
|
124
148
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
125
149
|
const ast = parse(DULOXETINE_SMILES);
|
|
126
|
-
const code = ast.toCode('v');
|
|
150
|
+
const code = ast.toCode('v', { verbose: true });
|
|
127
151
|
const executableCode = stripExports(code);
|
|
128
152
|
|
|
129
153
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -150,9 +174,15 @@ describe('Carbamazepine Integration Test', () => {
|
|
|
150
174
|
expect(code).toMatchSnapshot();
|
|
151
175
|
});
|
|
152
176
|
|
|
177
|
+
test('generates valid verbose code via toCode()', () => {
|
|
178
|
+
const ast = parse(CARBAMAZEPINE_SMILES);
|
|
179
|
+
const code = ast.toCode('v', { verbose: true });
|
|
180
|
+
expect(code).toMatchSnapshot();
|
|
181
|
+
});
|
|
182
|
+
|
|
153
183
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
154
184
|
const ast = parse(CARBAMAZEPINE_SMILES);
|
|
155
|
-
const code = ast.toCode('v');
|
|
185
|
+
const code = ast.toCode('v', { verbose: true });
|
|
156
186
|
const executableCode = stripExports(code);
|
|
157
187
|
|
|
158
188
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -179,9 +209,15 @@ describe('Valproic Acid Integration Test', () => {
|
|
|
179
209
|
expect(code).toMatchSnapshot();
|
|
180
210
|
});
|
|
181
211
|
|
|
212
|
+
test('generates valid verbose code via toCode()', () => {
|
|
213
|
+
const ast = parse(VALPROIC_ACID_SMILES);
|
|
214
|
+
const code = ast.toCode('v', { verbose: true });
|
|
215
|
+
expect(code).toMatchSnapshot();
|
|
216
|
+
});
|
|
217
|
+
|
|
182
218
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
183
219
|
const ast = parse(VALPROIC_ACID_SMILES);
|
|
184
|
-
const code = ast.toCode('v');
|
|
220
|
+
const code = ast.toCode('v', { verbose: true });
|
|
185
221
|
const executableCode = stripExports(code);
|
|
186
222
|
|
|
187
223
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -12,6 +12,7 @@ const EZETIMIBE_SMILES = 'C[C@H]1[C@@H](O)[C@H](O[C@H]1c2ccc(F)cc2)c3ccc(O)cc3C(
|
|
|
12
12
|
const FENOFIBRATE_SMILES = 'CC(C)OC(=O)C(C)(C)Oc1ccc(cc1)C(=O)c2ccc(Cl)cc2';
|
|
13
13
|
const GEMFIBROZIL_SMILES = 'CC1=CC(C)=CC=C1CCCC(C)(C)C(=O)O';
|
|
14
14
|
const PITAVASTATIN_SMILES = 'CC(C)c1nc(nc(c1/C=C/[C@@H](O)C[C@@H](O)CC(=O)O)c2ccc(F)cc2)NC(=O)C3CC3';
|
|
15
|
+
const CHOLESTEROL_SMILES = 'C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C';
|
|
15
16
|
|
|
16
17
|
describe('Atorvastatin Integration Test', () => {
|
|
17
18
|
test('parses atorvastatin', () => {
|
|
@@ -27,6 +28,12 @@ describe('Atorvastatin Integration Test', () => {
|
|
|
27
28
|
expect(code).toMatchSnapshot();
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
test('generates valid verbose code via toCode()', () => {
|
|
32
|
+
const ast = parse(ATORVASTATIN_SMILES);
|
|
33
|
+
const code = ast.toCode('v', { verbose: true });
|
|
34
|
+
expect(code).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
test('generated code is valid JavaScript', () => {
|
|
31
38
|
const ast = parse(ATORVASTATIN_SMILES);
|
|
32
39
|
const code = ast.toCode('v');
|
|
@@ -34,14 +41,14 @@ describe('Atorvastatin Integration Test', () => {
|
|
|
34
41
|
|
|
35
42
|
let factory;
|
|
36
43
|
expect(() => {
|
|
37
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
44
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
38
45
|
}).not.toThrow();
|
|
39
46
|
expect(typeof factory).toBe('function');
|
|
40
47
|
});
|
|
41
48
|
|
|
42
49
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
43
50
|
const ast = parse(ATORVASTATIN_SMILES);
|
|
44
|
-
const code = ast.toCode('v');
|
|
51
|
+
const code = ast.toCode('v', { verbose: true });
|
|
45
52
|
const executableCode = stripExports(code);
|
|
46
53
|
|
|
47
54
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -67,6 +74,12 @@ describe('Simvastatin Integration Test', () => {
|
|
|
67
74
|
expect(code).toMatchSnapshot();
|
|
68
75
|
});
|
|
69
76
|
|
|
77
|
+
test('generates valid verbose code via toCode()', () => {
|
|
78
|
+
const ast = parse(SIMVASTATIN_SMILES);
|
|
79
|
+
const code = ast.toCode('v', { verbose: true });
|
|
80
|
+
expect(code).toMatchSnapshot();
|
|
81
|
+
});
|
|
82
|
+
|
|
70
83
|
test('generated code is valid JavaScript', () => {
|
|
71
84
|
const ast = parse(SIMVASTATIN_SMILES);
|
|
72
85
|
const code = ast.toCode('v');
|
|
@@ -74,14 +87,14 @@ describe('Simvastatin Integration Test', () => {
|
|
|
74
87
|
|
|
75
88
|
let factory;
|
|
76
89
|
expect(() => {
|
|
77
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
90
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
78
91
|
}).not.toThrow();
|
|
79
92
|
expect(typeof factory).toBe('function');
|
|
80
93
|
});
|
|
81
94
|
|
|
82
95
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
83
96
|
const ast = parse(SIMVASTATIN_SMILES);
|
|
84
|
-
const code = ast.toCode('v');
|
|
97
|
+
const code = ast.toCode('v', { verbose: true });
|
|
85
98
|
const executableCode = stripExports(code);
|
|
86
99
|
|
|
87
100
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -107,6 +120,12 @@ describe('Rosuvastatin Integration Test', () => {
|
|
|
107
120
|
expect(code).toMatchSnapshot();
|
|
108
121
|
});
|
|
109
122
|
|
|
123
|
+
test('generates valid verbose code via toCode()', () => {
|
|
124
|
+
const ast = parse(ROSUVASTATIN_SMILES);
|
|
125
|
+
const code = ast.toCode('v', { verbose: true });
|
|
126
|
+
expect(code).toMatchSnapshot();
|
|
127
|
+
});
|
|
128
|
+
|
|
110
129
|
test('generated code is valid JavaScript', () => {
|
|
111
130
|
const ast = parse(ROSUVASTATIN_SMILES);
|
|
112
131
|
const code = ast.toCode('v');
|
|
@@ -114,14 +133,14 @@ describe('Rosuvastatin Integration Test', () => {
|
|
|
114
133
|
|
|
115
134
|
let factory;
|
|
116
135
|
expect(() => {
|
|
117
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
136
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
118
137
|
}).not.toThrow();
|
|
119
138
|
expect(typeof factory).toBe('function');
|
|
120
139
|
});
|
|
121
140
|
|
|
122
141
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
123
142
|
const ast = parse(ROSUVASTATIN_SMILES);
|
|
124
|
-
const code = ast.toCode('v');
|
|
143
|
+
const code = ast.toCode('v', { verbose: true });
|
|
125
144
|
const executableCode = stripExports(code);
|
|
126
145
|
|
|
127
146
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -147,6 +166,12 @@ describe('Pravastatin Integration Test', () => {
|
|
|
147
166
|
expect(code).toMatchSnapshot();
|
|
148
167
|
});
|
|
149
168
|
|
|
169
|
+
test('generates valid verbose code via toCode()', () => {
|
|
170
|
+
const ast = parse(PRAVASTATIN_SMILES);
|
|
171
|
+
const code = ast.toCode('v', { verbose: true });
|
|
172
|
+
expect(code).toMatchSnapshot();
|
|
173
|
+
});
|
|
174
|
+
|
|
150
175
|
test('generated code is valid JavaScript', () => {
|
|
151
176
|
const ast = parse(PRAVASTATIN_SMILES);
|
|
152
177
|
const code = ast.toCode('v');
|
|
@@ -154,14 +179,14 @@ describe('Pravastatin Integration Test', () => {
|
|
|
154
179
|
|
|
155
180
|
let factory;
|
|
156
181
|
expect(() => {
|
|
157
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
182
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
158
183
|
}).not.toThrow();
|
|
159
184
|
expect(typeof factory).toBe('function');
|
|
160
185
|
});
|
|
161
186
|
|
|
162
187
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
163
188
|
const ast = parse(PRAVASTATIN_SMILES);
|
|
164
|
-
const code = ast.toCode('v');
|
|
189
|
+
const code = ast.toCode('v', { verbose: true });
|
|
165
190
|
const executableCode = stripExports(code);
|
|
166
191
|
|
|
167
192
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -187,6 +212,12 @@ describe('Lovastatin Integration Test', () => {
|
|
|
187
212
|
expect(code).toMatchSnapshot();
|
|
188
213
|
});
|
|
189
214
|
|
|
215
|
+
test('generates valid verbose code via toCode()', () => {
|
|
216
|
+
const ast = parse(LOVASTATIN_SMILES);
|
|
217
|
+
const code = ast.toCode('v', { verbose: true });
|
|
218
|
+
expect(code).toMatchSnapshot();
|
|
219
|
+
});
|
|
220
|
+
|
|
190
221
|
test('generated code is valid JavaScript', () => {
|
|
191
222
|
const ast = parse(LOVASTATIN_SMILES);
|
|
192
223
|
const code = ast.toCode('v');
|
|
@@ -194,14 +225,14 @@ describe('Lovastatin Integration Test', () => {
|
|
|
194
225
|
|
|
195
226
|
let factory;
|
|
196
227
|
expect(() => {
|
|
197
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
228
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
198
229
|
}).not.toThrow();
|
|
199
230
|
expect(typeof factory).toBe('function');
|
|
200
231
|
});
|
|
201
232
|
|
|
202
233
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
203
234
|
const ast = parse(LOVASTATIN_SMILES);
|
|
204
|
-
const code = ast.toCode('v');
|
|
235
|
+
const code = ast.toCode('v', { verbose: true });
|
|
205
236
|
const executableCode = stripExports(code);
|
|
206
237
|
|
|
207
238
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -227,6 +258,12 @@ describe('Fluvastatin Integration Test', () => {
|
|
|
227
258
|
expect(code).toMatchSnapshot();
|
|
228
259
|
});
|
|
229
260
|
|
|
261
|
+
test('generates valid verbose code via toCode()', () => {
|
|
262
|
+
const ast = parse(FLUVASTATIN_SMILES);
|
|
263
|
+
const code = ast.toCode('v', { verbose: true });
|
|
264
|
+
expect(code).toMatchSnapshot();
|
|
265
|
+
});
|
|
266
|
+
|
|
230
267
|
test('generated code is valid JavaScript', () => {
|
|
231
268
|
const ast = parse(FLUVASTATIN_SMILES);
|
|
232
269
|
const code = ast.toCode('v');
|
|
@@ -234,14 +271,14 @@ describe('Fluvastatin Integration Test', () => {
|
|
|
234
271
|
|
|
235
272
|
let factory;
|
|
236
273
|
expect(() => {
|
|
237
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
274
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
238
275
|
}).not.toThrow();
|
|
239
276
|
expect(typeof factory).toBe('function');
|
|
240
277
|
});
|
|
241
278
|
|
|
242
279
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
243
280
|
const ast = parse(FLUVASTATIN_SMILES);
|
|
244
|
-
const code = ast.toCode('v');
|
|
281
|
+
const code = ast.toCode('v', { verbose: true });
|
|
245
282
|
const executableCode = stripExports(code);
|
|
246
283
|
|
|
247
284
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -267,6 +304,12 @@ describe('Ezetimibe Integration Test', () => {
|
|
|
267
304
|
expect(code).toMatchSnapshot();
|
|
268
305
|
});
|
|
269
306
|
|
|
307
|
+
test('generates valid verbose code via toCode()', () => {
|
|
308
|
+
const ast = parse(EZETIMIBE_SMILES);
|
|
309
|
+
const code = ast.toCode('v', { verbose: true });
|
|
310
|
+
expect(code).toMatchSnapshot();
|
|
311
|
+
});
|
|
312
|
+
|
|
270
313
|
test('generated code is valid JavaScript', () => {
|
|
271
314
|
const ast = parse(EZETIMIBE_SMILES);
|
|
272
315
|
const code = ast.toCode('v');
|
|
@@ -274,14 +317,14 @@ describe('Ezetimibe Integration Test', () => {
|
|
|
274
317
|
|
|
275
318
|
let factory;
|
|
276
319
|
expect(() => {
|
|
277
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
320
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
278
321
|
}).not.toThrow();
|
|
279
322
|
expect(typeof factory).toBe('function');
|
|
280
323
|
});
|
|
281
324
|
|
|
282
325
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
283
326
|
const ast = parse(EZETIMIBE_SMILES);
|
|
284
|
-
const code = ast.toCode('v');
|
|
327
|
+
const code = ast.toCode('v', { verbose: true });
|
|
285
328
|
const executableCode = stripExports(code);
|
|
286
329
|
|
|
287
330
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -307,6 +350,12 @@ describe('Fenofibrate Integration Test', () => {
|
|
|
307
350
|
expect(code).toMatchSnapshot();
|
|
308
351
|
});
|
|
309
352
|
|
|
353
|
+
test('generates valid verbose code via toCode()', () => {
|
|
354
|
+
const ast = parse(FENOFIBRATE_SMILES);
|
|
355
|
+
const code = ast.toCode('v', { verbose: true });
|
|
356
|
+
expect(code).toMatchSnapshot();
|
|
357
|
+
});
|
|
358
|
+
|
|
310
359
|
test('generated code is valid JavaScript', () => {
|
|
311
360
|
const ast = parse(FENOFIBRATE_SMILES);
|
|
312
361
|
const code = ast.toCode('v');
|
|
@@ -314,14 +363,14 @@ describe('Fenofibrate Integration Test', () => {
|
|
|
314
363
|
|
|
315
364
|
let factory;
|
|
316
365
|
expect(() => {
|
|
317
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
366
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
318
367
|
}).not.toThrow();
|
|
319
368
|
expect(typeof factory).toBe('function');
|
|
320
369
|
});
|
|
321
370
|
|
|
322
371
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
323
372
|
const ast = parse(FENOFIBRATE_SMILES);
|
|
324
|
-
const code = ast.toCode('v');
|
|
373
|
+
const code = ast.toCode('v', { verbose: true });
|
|
325
374
|
const executableCode = stripExports(code);
|
|
326
375
|
|
|
327
376
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -347,6 +396,12 @@ describe('Gemfibrozil Integration Test', () => {
|
|
|
347
396
|
expect(code).toMatchSnapshot();
|
|
348
397
|
});
|
|
349
398
|
|
|
399
|
+
test('generates valid verbose code via toCode()', () => {
|
|
400
|
+
const ast = parse(GEMFIBROZIL_SMILES);
|
|
401
|
+
const code = ast.toCode('v', { verbose: true });
|
|
402
|
+
expect(code).toMatchSnapshot();
|
|
403
|
+
});
|
|
404
|
+
|
|
350
405
|
test('generated code is valid JavaScript', () => {
|
|
351
406
|
const ast = parse(GEMFIBROZIL_SMILES);
|
|
352
407
|
const code = ast.toCode('v');
|
|
@@ -354,14 +409,14 @@ describe('Gemfibrozil Integration Test', () => {
|
|
|
354
409
|
|
|
355
410
|
let factory;
|
|
356
411
|
expect(() => {
|
|
357
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
412
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
358
413
|
}).not.toThrow();
|
|
359
414
|
expect(typeof factory).toBe('function');
|
|
360
415
|
});
|
|
361
416
|
|
|
362
417
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
363
418
|
const ast = parse(GEMFIBROZIL_SMILES);
|
|
364
|
-
const code = ast.toCode('v');
|
|
419
|
+
const code = ast.toCode('v', { verbose: true });
|
|
365
420
|
const executableCode = stripExports(code);
|
|
366
421
|
|
|
367
422
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -387,6 +442,12 @@ describe('Pitavastatin Integration Test', () => {
|
|
|
387
442
|
expect(code).toMatchSnapshot();
|
|
388
443
|
});
|
|
389
444
|
|
|
445
|
+
test('generates valid verbose code via toCode()', () => {
|
|
446
|
+
const ast = parse(PITAVASTATIN_SMILES);
|
|
447
|
+
const code = ast.toCode('v', { verbose: true });
|
|
448
|
+
expect(code).toMatchSnapshot();
|
|
449
|
+
});
|
|
450
|
+
|
|
390
451
|
test('generated code is valid JavaScript', () => {
|
|
391
452
|
const ast = parse(PITAVASTATIN_SMILES);
|
|
392
453
|
const code = ast.toCode('v');
|
|
@@ -394,14 +455,14 @@ describe('Pitavastatin Integration Test', () => {
|
|
|
394
455
|
|
|
395
456
|
let factory;
|
|
396
457
|
expect(() => {
|
|
397
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
458
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
398
459
|
}).not.toThrow();
|
|
399
460
|
expect(typeof factory).toBe('function');
|
|
400
461
|
});
|
|
401
462
|
|
|
402
463
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
403
464
|
const ast = parse(PITAVASTATIN_SMILES);
|
|
404
|
-
const code = ast.toCode('v');
|
|
465
|
+
const code = ast.toCode('v', { verbose: true });
|
|
405
466
|
const executableCode = stripExports(code);
|
|
406
467
|
|
|
407
468
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
@@ -412,3 +473,49 @@ describe('Pitavastatin Integration Test', () => {
|
|
|
412
473
|
expect(reconstructed.smiles).toBe(PITAVASTATIN_SMILES);
|
|
413
474
|
});
|
|
414
475
|
});
|
|
476
|
+
|
|
477
|
+
describe('Cholesterol Integration Test', () => {
|
|
478
|
+
test('parses cholesterol', () => {
|
|
479
|
+
const ast = parse(CHOLESTEROL_SMILES);
|
|
480
|
+
const obj = ast.toObject();
|
|
481
|
+
expect(obj).toMatchSnapshot();
|
|
482
|
+
expect(ast.smiles).toBe(CHOLESTEROL_SMILES);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('generates valid code via toCode()', () => {
|
|
486
|
+
const ast = parse(CHOLESTEROL_SMILES);
|
|
487
|
+
const code = ast.toCode('v');
|
|
488
|
+
expect(code).toMatchSnapshot();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('generates valid verbose code via toCode()', () => {
|
|
492
|
+
const ast = parse(CHOLESTEROL_SMILES);
|
|
493
|
+
const code = ast.toCode('v', { verbose: true });
|
|
494
|
+
expect(code).toMatchSnapshot();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('generated code is valid JavaScript', () => {
|
|
498
|
+
const ast = parse(CHOLESTEROL_SMILES);
|
|
499
|
+
const code = ast.toCode('v');
|
|
500
|
+
const executableCode = stripExports(code);
|
|
501
|
+
|
|
502
|
+
let factory;
|
|
503
|
+
expect(() => {
|
|
504
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
505
|
+
}).not.toThrow();
|
|
506
|
+
expect(typeof factory).toBe('function');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
510
|
+
const ast = parse(CHOLESTEROL_SMILES);
|
|
511
|
+
const code = ast.toCode('v', { verbose: true });
|
|
512
|
+
const executableCode = stripExports(code);
|
|
513
|
+
|
|
514
|
+
const varMatch = code.match(/export const (v\d+) = /g);
|
|
515
|
+
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1] : 'v1';
|
|
516
|
+
|
|
517
|
+
const reconstructed = executeCode(executableCode, lastVar);
|
|
518
|
+
|
|
519
|
+
expect(reconstructed.smiles).toBe(CHOLESTEROL_SMILES);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { parse } from '../src/parser/index.js';
|
|
3
|
+
import { codegenRoundTrip } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const CHOLESTEROL_SMILES = 'C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C';
|
|
6
|
+
|
|
7
|
+
describe('Cholesterol - Divide and Conquer', () => {
|
|
8
|
+
// Cholesterol SMILES:
|
|
9
|
+
// C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C
|
|
10
|
+
//
|
|
11
|
+
// The failure: ring 4 is being dropped
|
|
12
|
+
// Expected: ...CC=C4[C@@]3(CC[C@@H](C4)O)C)C
|
|
13
|
+
// Received: ...CC=C[C@@]3(CC[C@@H](C)O)C)C
|
|
14
|
+
|
|
15
|
+
// Level 1: Simple ring closures
|
|
16
|
+
test('simple ring', () => {
|
|
17
|
+
const s = 'C1CCCCC1';
|
|
18
|
+
expect(parse(s).smiles).toBe(s);
|
|
19
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Level 2: Two fused rings (decalin-like)
|
|
23
|
+
test('two fused rings', () => {
|
|
24
|
+
const s = 'C1CCC2CCCCC2C1';
|
|
25
|
+
expect(parse(s).smiles).toBe(s);
|
|
26
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Level 3: Ring closure inside a branch - the core pattern
|
|
30
|
+
test('ring closure in branch: C1CCC(C1)C', () => {
|
|
31
|
+
const s = 'C1CCC(C1)C';
|
|
32
|
+
expect(parse(s).smiles).toBe(s);
|
|
33
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Level 4: Two ring numbers, one closing in a branch
|
|
37
|
+
test('two rings, one closing in branch: C1CC2CCC(C2)C1', () => {
|
|
38
|
+
const s = 'C1CC2CCC(C2)C1';
|
|
39
|
+
expect(parse(s).smiles).toBe(s);
|
|
40
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Level 5: Ring opening and closing with different numbers in nested context
|
|
44
|
+
test('ring 4 opens and closes in branch: C4CCC(C4)', () => {
|
|
45
|
+
const s = 'C4CCC(C4)';
|
|
46
|
+
expect(parse(s).smiles).toBe(s);
|
|
47
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Level 6: The specific pattern from cholesterol - ring opens, then closes inside a parenthesized branch
|
|
51
|
+
test('ring open at atom, close inside branch argument: CC=C4C(CC(C4)O)', () => {
|
|
52
|
+
const s = 'CC=C4C(CC(C4)O)';
|
|
53
|
+
expect(parse(s).smiles).toBe(s);
|
|
54
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Level 7: Closer to cholesterol D-ring pattern
|
|
58
|
+
test('cholesterol D-ring pattern: CC=C4C(CCC(C4)O)C', () => {
|
|
59
|
+
const s = 'CC=C4C(CCC(C4)O)C';
|
|
60
|
+
expect(parse(s).smiles).toBe(s);
|
|
61
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Level 8: Add ring 3 fused with ring 4
|
|
65
|
+
test('fused rings 3+4: C3CC=C4C3(CCC(C4)O)C', () => {
|
|
66
|
+
const s = 'C3CC=C4C3(CCC(C4)O)C';
|
|
67
|
+
expect(parse(s).smiles).toBe(s);
|
|
68
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Level 9: The C/D ring system from cholesterol (without stereo)
|
|
72
|
+
test('C/D ring system: C2CC=C4C(CC2)(CCC(C4)O)C', () => {
|
|
73
|
+
const s = 'C2CC=C4C(CC2)(CCC(C4)O)C';
|
|
74
|
+
expect(parse(s).smiles).toBe(s);
|
|
75
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Level 10: Three fused rings (A/B/C ring without stereo)
|
|
79
|
+
test('three fused rings: C1CCC2C1CCC3CC=C4C3(CCC(C4)O)C', () => {
|
|
80
|
+
const s = 'C1CCC2C1CCC3C2CC=C4C3(CCC(C4)O)C';
|
|
81
|
+
expect(parse(s).smiles).toBe(s);
|
|
82
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Level 11: Isolate multiple branches on same atom with ring closures
|
|
86
|
+
test('two branches with ring closures: C1C(C1)(C2)CC2', () => {
|
|
87
|
+
const s = 'C1C(C1)(C2)CC2';
|
|
88
|
+
expect(parse(s).smiles).toBe(s);
|
|
89
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Level 12: Minimal failing pattern - ring closure in second branch
|
|
93
|
+
test('ring close in second branch: C4CC(C)(C4)', () => {
|
|
94
|
+
const s = 'C4CC(C)(C4)';
|
|
95
|
+
expect(parse(s).smiles).toBe(s);
|
|
96
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Level 13: Full cholesterol (no stereo) to isolate stereo vs ring issue
|
|
100
|
+
test('cholesterol without stereochemistry', () => {
|
|
101
|
+
const s = 'CC(CCCC(C)C)C1CCC2C1(CCC3C2CC=C4C3(CCC(C4)O)C)C';
|
|
102
|
+
expect(parse(s).smiles).toBe(s);
|
|
103
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Level 14: Full cholesterol with stereochemistry
|
|
107
|
+
test('full cholesterol', () => {
|
|
108
|
+
const s = CHOLESTEROL_SMILES;
|
|
109
|
+
expect(parse(s).smiles).toBe(s);
|
|
110
|
+
expect(codegenRoundTrip(s).smiles).toBe(s);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -24,6 +24,12 @@ describe('Dexamethasone Integration Test', () => {
|
|
|
24
24
|
expect(code).toMatchSnapshot();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
test('generates valid verbose code via toCode()', () => {
|
|
28
|
+
const ast = parse(DEXAMETHASONE_SMILES);
|
|
29
|
+
const code = ast.toCode('v', { verbose: true });
|
|
30
|
+
expect(code).toMatchSnapshot();
|
|
31
|
+
});
|
|
32
|
+
|
|
27
33
|
test('generated code is valid JavaScript', () => {
|
|
28
34
|
const ast = parse(DEXAMETHASONE_SMILES);
|
|
29
35
|
const code = ast.toCode('v');
|
|
@@ -31,14 +37,14 @@ describe('Dexamethasone Integration Test', () => {
|
|
|
31
37
|
|
|
32
38
|
let factory;
|
|
33
39
|
expect(() => {
|
|
34
|
-
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
40
|
+
factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
|
|
35
41
|
}).not.toThrow();
|
|
36
42
|
expect(typeof factory).toBe('function');
|
|
37
43
|
});
|
|
38
44
|
|
|
39
45
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
40
46
|
const ast = parse(DEXAMETHASONE_SMILES);
|
|
41
|
-
const code = ast.toCode('v');
|
|
47
|
+
const code = ast.toCode('v', { verbose: true });
|
|
42
48
|
const executableCode = stripExports(code);
|
|
43
49
|
const varMatch = code.match(/export const (v\d+) = /g);
|
|
44
50
|
const lastVar = varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1];
|