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
package/src/decompiler.test.js
CHANGED
|
@@ -15,42 +15,42 @@ function rawFusedRing(rings) {
|
|
|
15
15
|
return { type: 'fused_ring', rings };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
describe('Decompiler - Ring', () => {
|
|
18
|
+
describe('Decompiler - Ring (verbose)', () => {
|
|
19
19
|
test('decompiles simple ring', () => {
|
|
20
20
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
21
|
-
const code = decompile(benzene);
|
|
21
|
+
const code = decompile(benzene, { verbose: true });
|
|
22
22
|
expect(code).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
test('decompiles ring with custom ring number', () => {
|
|
26
26
|
const ring = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
27
|
-
const code = decompile(ring);
|
|
27
|
+
const code = decompile(ring, { verbose: true });
|
|
28
28
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
test('decompiles ring with offset', () => {
|
|
32
32
|
const ring = Ring({ atoms: 'C', size: 6, offset: 2 });
|
|
33
|
-
const code = decompile(ring);
|
|
33
|
+
const code = decompile(ring, { verbose: true });
|
|
34
34
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, offset: 2 });");
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
test('decompiles ring with bonds', () => {
|
|
38
38
|
const ring = Ring({ atoms: 'C', size: 6, bonds: ['=', null, '=', null, '='] });
|
|
39
|
-
const code = decompile(ring);
|
|
39
|
+
const code = decompile(ring, { verbose: true });
|
|
40
40
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, bonds: ['=', null, '=', null, '='] });");
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
test('decompiles ring with branchDepths metadata', () => {
|
|
44
44
|
const ring = Ring({ atoms: 'C', size: 6 });
|
|
45
45
|
ring.metaBranchDepths = [0, 0, 0, 1, 1, 1];
|
|
46
|
-
const code = decompile(ring);
|
|
46
|
+
const code = decompile(ring, { verbose: true });
|
|
47
47
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, branchDepths: [0, 0, 0, 1, 1, 1] });");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('decompiles ring with substitutions', () => {
|
|
51
51
|
const ring = Ring({ atoms: 'C', size: 6 });
|
|
52
52
|
const substituted = ring.substitute(2, 'N');
|
|
53
|
-
const code = decompile(substituted);
|
|
53
|
+
const code = decompile(substituted, { verbose: true });
|
|
54
54
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
55
55
|
export const v2 = v1.substitute(2, 'N');`);
|
|
56
56
|
});
|
|
@@ -59,7 +59,7 @@ export const v2 = v1.substitute(2, 'N');`);
|
|
|
59
59
|
const ring = Ring({ atoms: 'C', size: 6 });
|
|
60
60
|
const methyl = Linear(['C']);
|
|
61
61
|
const attached = ring.attach(1, methyl);
|
|
62
|
-
const code = decompile(attached);
|
|
62
|
+
const code = decompile(attached, { verbose: true });
|
|
63
63
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
64
64
|
export const v2 = Linear(['C']);
|
|
65
65
|
export const v3 = v1.attach(1, v2);`);
|
|
@@ -71,7 +71,7 @@ export const v3 = v1.attach(1, v2);`);
|
|
|
71
71
|
const branch2 = Linear(['O']);
|
|
72
72
|
const withAttach = ring.attach(2, branch1);
|
|
73
73
|
const withBoth = withAttach.attach(4, branch2);
|
|
74
|
-
const code = decompile(withBoth);
|
|
74
|
+
const code = decompile(withBoth, { verbose: true });
|
|
75
75
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
76
76
|
export const v2 = Linear(['N']);
|
|
77
77
|
export const v3 = v1.attach(2, v2);
|
|
@@ -79,35 +79,98 @@ export const v4 = Linear(['O']);
|
|
|
79
79
|
export const v5 = v3.attach(4, v4);`);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test('uses toCode() method', () => {
|
|
82
|
+
test('uses toCode() method with verbose', () => {
|
|
83
83
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
84
|
-
expect(benzene.toCode()).toBe("export const ring1 = Ring({ atoms: 'c', size: 6 });");
|
|
85
|
-
expect(benzene.toCode('r')).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
84
|
+
expect(benzene.toCode('ring', { verbose: true })).toBe("export const ring1 = Ring({ atoms: 'c', size: 6 });");
|
|
85
|
+
expect(benzene.toCode('r', { verbose: true })).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
describe('Decompiler -
|
|
89
|
+
describe('Decompiler - Ring (non-verbose)', () => {
|
|
90
|
+
test('decompiles simple ring as Fragment', () => {
|
|
91
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
92
|
+
const code = decompile(benzene);
|
|
93
|
+
expect(code).toBe("export const v1 = Fragment('c1ccccc1');");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('decompiles ring with custom ring number as Fragment', () => {
|
|
97
|
+
const ring = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
98
|
+
const code = decompile(ring);
|
|
99
|
+
expect(code).toBe("export const v1 = Fragment('C2CCCCC2');");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('decompiles ring with bonds as Fragment', () => {
|
|
103
|
+
const ring = Ring({ atoms: 'C', size: 6, bonds: ['=', null, '=', null, '='] });
|
|
104
|
+
const code = decompile(ring);
|
|
105
|
+
expect(code).toBe("export const v1 = Fragment('C1=CC=CC=C1');");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('decompiles ring with branchDepths as Fragment', () => {
|
|
109
|
+
const ring = Ring({ atoms: 'C', size: 6 });
|
|
110
|
+
ring.metaBranchDepths = [0, 0, 0, 1, 1, 1];
|
|
111
|
+
const code = decompile(ring);
|
|
112
|
+
expect(code).toBe("export const v1 = Fragment('C1CC(CCC1)');");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('decompiles ring with substitutions as Fragment', () => {
|
|
116
|
+
const ring = Ring({ atoms: 'C', size: 6 });
|
|
117
|
+
const substituted = ring.substitute(2, 'N');
|
|
118
|
+
const code = decompile(substituted);
|
|
119
|
+
expect(code).toBe(`export const v1 = Fragment('C1CCCCC1');
|
|
120
|
+
export const v2 = Fragment('C1NCCCC1');`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('decompiles ring with single attachment using Fragment', () => {
|
|
124
|
+
const ring = Ring({ atoms: 'C', size: 6 });
|
|
125
|
+
const methyl = Linear(['C']);
|
|
126
|
+
const attached = ring.attach(1, methyl);
|
|
127
|
+
const code = decompile(attached);
|
|
128
|
+
expect(code).toBe(`export const v1 = Fragment('C1CCCCC1');
|
|
129
|
+
export const v2 = Fragment('C');
|
|
130
|
+
export const v3 = v1.attach(1, v2);`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('decompiles ring with substitution and attachment using Fragment', () => {
|
|
134
|
+
const ring = Ring({ atoms: 'C', size: 6 });
|
|
135
|
+
const substituted = ring.substitute(2, 'N');
|
|
136
|
+
const branch = Linear(['O']);
|
|
137
|
+
const attached = substituted.attach(1, branch);
|
|
138
|
+
const code = decompile(attached);
|
|
139
|
+
expect(code).toBe(`export const v1 = Fragment('C1CCCCC1');
|
|
140
|
+
export const v2 = Fragment('C1NCCCC1');
|
|
141
|
+
export const v3 = Fragment('O');
|
|
142
|
+
export const v4 = v2.attach(1, v3);`);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('uses toCode() non-verbose by default', () => {
|
|
146
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
147
|
+
expect(benzene.toCode()).toBe("export const ring1 = Fragment('c1ccccc1');");
|
|
148
|
+
expect(benzene.toCode('r')).toBe("export const r1 = Fragment('c1ccccc1');");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Decompiler - Linear (verbose)', () => {
|
|
90
153
|
test('decompiles simple linear chain', () => {
|
|
91
154
|
const propane = Linear(['C', 'C', 'C']);
|
|
92
|
-
const code = decompile(propane);
|
|
155
|
+
const code = decompile(propane, { verbose: true });
|
|
93
156
|
expect(code).toBe("export const v1 = Linear(['C', 'C', 'C']);");
|
|
94
157
|
});
|
|
95
158
|
|
|
96
159
|
test('decompiles single-atom linear', () => {
|
|
97
160
|
const linear = Linear(['C']);
|
|
98
|
-
const code = decompile(linear);
|
|
161
|
+
const code = decompile(linear, { verbose: true });
|
|
99
162
|
expect(code).toBe("export const v1 = Linear(['C']);");
|
|
100
163
|
});
|
|
101
164
|
|
|
102
165
|
test('decompiles linear with bonds', () => {
|
|
103
166
|
const ethene = Linear(['C', 'C'], ['=']);
|
|
104
|
-
const code = decompile(ethene);
|
|
167
|
+
const code = decompile(ethene, { verbose: true });
|
|
105
168
|
expect(code).toBe("export const v1 = Linear(['C', 'C'], ['=']);");
|
|
106
169
|
});
|
|
107
170
|
|
|
108
171
|
test('decompiles linear with different atom types and bond', () => {
|
|
109
172
|
const linear = Linear(['C', 'N'], ['=']);
|
|
110
|
-
const code = decompile(linear);
|
|
173
|
+
const code = decompile(linear, { verbose: true });
|
|
111
174
|
expect(code).toBe("export const v1 = Linear(['C', 'N'], ['=']);");
|
|
112
175
|
});
|
|
113
176
|
|
|
@@ -115,27 +178,69 @@ describe('Decompiler - Linear', () => {
|
|
|
115
178
|
const chain = Linear(['C', 'C', 'C']);
|
|
116
179
|
const branch = Linear(['N']);
|
|
117
180
|
const attached = chain.attach(2, branch);
|
|
118
|
-
const code = decompile(attached);
|
|
181
|
+
const code = decompile(attached, { verbose: true });
|
|
119
182
|
expect(code).toBe(`export const v1 = Linear(['C', 'C', 'C']);
|
|
120
183
|
export const v2 = Linear(['N']);
|
|
121
184
|
export const v3 = v1.attach(2, v2);`);
|
|
122
185
|
});
|
|
123
186
|
|
|
124
|
-
test('uses toCode() method', () => {
|
|
187
|
+
test('uses toCode() method with verbose', () => {
|
|
188
|
+
const propane = Linear(['C', 'C', 'C']);
|
|
189
|
+
expect(propane.toCode('linear', { verbose: true })).toBe("export const linear1 = Linear(['C', 'C', 'C']);");
|
|
190
|
+
expect(propane.toCode('c', { verbose: true })).toBe("export const c1 = Linear(['C', 'C', 'C']);");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Decompiler - Linear (non-verbose)', () => {
|
|
195
|
+
test('decompiles simple linear as Fragment', () => {
|
|
196
|
+
const propane = Linear(['C', 'C', 'C']);
|
|
197
|
+
const code = decompile(propane);
|
|
198
|
+
expect(code).toBe("export const v1 = Fragment('CCC');");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('decompiles single-atom linear as Fragment', () => {
|
|
202
|
+
const linear = Linear(['C']);
|
|
203
|
+
const code = decompile(linear);
|
|
204
|
+
expect(code).toBe("export const v1 = Fragment('C');");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('keeps linear with bonds as Linear (not Fragment)', () => {
|
|
208
|
+
const ethene = Linear(['C', 'C'], ['=']);
|
|
209
|
+
const code = decompile(ethene);
|
|
210
|
+
expect(code).toBe("export const v1 = Linear(['C', 'C'], ['=']);");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('keeps linear with leadingBond as Linear (not Fragment)', () => {
|
|
214
|
+
const linear = Linear(['C', 'C'], [], {}, '=');
|
|
215
|
+
const code = decompile(linear);
|
|
216
|
+
expect(code).toBe("export const v1 = Linear(['C', 'C'], [], {}, '=');");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('decompiles linear with attachments using Fragment', () => {
|
|
220
|
+
const chain = Linear(['C', 'C', 'C']);
|
|
221
|
+
const branch = Linear(['N']);
|
|
222
|
+
const attached = chain.attach(2, branch);
|
|
223
|
+
const code = decompile(attached);
|
|
224
|
+
expect(code).toBe(`export const v1 = Fragment('CCC');
|
|
225
|
+
export const v2 = Fragment('N');
|
|
226
|
+
export const v3 = v1.attach(2, v2);`);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('uses toCode() non-verbose by default', () => {
|
|
125
230
|
const propane = Linear(['C', 'C', 'C']);
|
|
126
|
-
expect(propane.toCode()).toBe("export const linear1 =
|
|
127
|
-
expect(propane.toCode('c')).toBe("export const c1 =
|
|
231
|
+
expect(propane.toCode()).toBe("export const linear1 = Fragment('CCC');");
|
|
232
|
+
expect(propane.toCode('c')).toBe("export const c1 = Fragment('CCC');");
|
|
128
233
|
});
|
|
129
234
|
});
|
|
130
235
|
|
|
131
|
-
describe('Decompiler - FusedRing (simple path)', () => {
|
|
236
|
+
describe('Decompiler - FusedRing (simple path, verbose)', () => {
|
|
132
237
|
test('decompiles 2-ring fused ring system via rawFusedRing', () => {
|
|
133
238
|
const ring1 = Ring({ atoms: 'C', size: 10, ringNumber: 1 });
|
|
134
239
|
const ring2 = Ring({
|
|
135
240
|
atoms: 'C', size: 6, ringNumber: 2, offset: 2,
|
|
136
241
|
});
|
|
137
242
|
const fusedRing = rawFusedRing([ring1, ring2]);
|
|
138
|
-
const code = decompile(fusedRing);
|
|
243
|
+
const code = decompile(fusedRing, { verbose: true });
|
|
139
244
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 10 });
|
|
140
245
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2 });
|
|
141
246
|
export const v3 = v1.fuse(2, v2);`);
|
|
@@ -148,7 +253,7 @@ export const v3 = v1.fuse(2, v2);`);
|
|
|
148
253
|
});
|
|
149
254
|
const fused = rawFusedRing([ring1, ring2]);
|
|
150
255
|
fused.metaLeadingBond = '=';
|
|
151
|
-
const code = decompile(fused);
|
|
256
|
+
const code = decompile(fused, { verbose: true });
|
|
152
257
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
153
258
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
154
259
|
export const v3 = v1.fuse(1, v2, { leadingBond: '=' });`);
|
|
@@ -164,7 +269,7 @@ export const v3 = v1.fuse(1, v2, { leadingBond: '=' });`);
|
|
|
164
269
|
});
|
|
165
270
|
const fused = rawFusedRing([ring1, ring2, ring3]);
|
|
166
271
|
fused.metaLeadingBond = '#';
|
|
167
|
-
const code = decompile(fused);
|
|
272
|
+
const code = decompile(fused, { verbose: true });
|
|
168
273
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
169
274
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
170
275
|
export const v3 = Ring({ atoms: 'C', size: 6, ringNumber: 3, offset: 2 });
|
|
@@ -181,7 +286,7 @@ export const v5 = v4.addRing(2, v3);`);
|
|
|
181
286
|
atoms: 'C', size: 6, ringNumber: 3, offset: 2,
|
|
182
287
|
});
|
|
183
288
|
const fused = rawFusedRing([ring1, ring2, ring3]);
|
|
184
|
-
const code = decompile(fused);
|
|
289
|
+
const code = decompile(fused, { verbose: true });
|
|
185
290
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
186
291
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
187
292
|
export const v3 = Ring({ atoms: 'C', size: 6, ringNumber: 3, offset: 2 });
|
|
@@ -192,14 +297,14 @@ export const v5 = v4.addRing(2, v3);`);
|
|
|
192
297
|
test('decompiles single-ring FusedRing via simple path', () => {
|
|
193
298
|
const ring1 = Ring({ atoms: 'C', size: 6 });
|
|
194
299
|
const fused = { type: 'fused_ring', rings: [ring1] };
|
|
195
|
-
const code = decompile(fused);
|
|
300
|
+
const code = decompile(fused, { verbose: true });
|
|
196
301
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
|
|
197
302
|
});
|
|
198
303
|
|
|
199
304
|
test('decompiles single-ring FusedRing with empty sequential rings', () => {
|
|
200
305
|
const ring1 = Ring({ atoms: 'C', size: 6 });
|
|
201
306
|
const fused = { type: 'fused_ring', rings: [ring1], metaSequentialRings: [] };
|
|
202
|
-
const code = decompile(fused);
|
|
307
|
+
const code = decompile(fused, { verbose: true });
|
|
203
308
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
|
|
204
309
|
});
|
|
205
310
|
|
|
@@ -210,14 +315,35 @@ export const v5 = v4.addRing(2, v3);`);
|
|
|
210
315
|
});
|
|
211
316
|
const fused = rawFusedRing([ring1, ring2]);
|
|
212
317
|
fused.metaSequentialRings = [];
|
|
213
|
-
const code = decompile(fused);
|
|
318
|
+
const code = decompile(fused, { verbose: true });
|
|
214
319
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
215
320
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
216
321
|
export const v3 = v1.fuse(1, v2);`);
|
|
217
322
|
});
|
|
218
323
|
});
|
|
219
324
|
|
|
220
|
-
describe('Decompiler - FusedRing (
|
|
325
|
+
describe('Decompiler - FusedRing (simple path, non-verbose)', () => {
|
|
326
|
+
test('decompiles 2-ring rawFusedRing as Fragment', () => {
|
|
327
|
+
const ring1 = Ring({ atoms: 'C', size: 10, ringNumber: 1 });
|
|
328
|
+
const ring2 = Ring({
|
|
329
|
+
atoms: 'C', size: 6, ringNumber: 2, offset: 2,
|
|
330
|
+
});
|
|
331
|
+
const fusedRing = rawFusedRing([ring1, ring2]);
|
|
332
|
+
const code = decompile(fusedRing);
|
|
333
|
+
expect(code).toBe(`export const v1 = Fragment('C1CCCCCCCCC1');
|
|
334
|
+
export const v2 = Fragment('C2CCCCC2');
|
|
335
|
+
export const v3 = v1.fuse(2, v2);`);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('decompiles single-ring FusedRing as Fragment', () => {
|
|
339
|
+
const ring1 = Ring({ atoms: 'C', size: 6 });
|
|
340
|
+
const fused = { type: 'fused_ring', rings: [ring1] };
|
|
341
|
+
const code = decompile(fused);
|
|
342
|
+
expect(code).toBe("export const v1 = Fragment('C1CCCCC1');");
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('Decompiler - FusedRing (interleaved/complex path, verbose)', () => {
|
|
221
347
|
// When .fuse() produces parser metadata (interleaved positions), the decompiler
|
|
222
348
|
// should emit FusedRing({ metadata: { rings: [...] } }) with hierarchical colocated atoms.
|
|
223
349
|
// No scattered Maps, no mutations.
|
|
@@ -230,7 +356,7 @@ describe('Decompiler - FusedRing (interleaved/complex path)', () => {
|
|
|
230
356
|
const ring1 = Ring({ atoms: 'C', size: 6 });
|
|
231
357
|
const ring2 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
232
358
|
const fused = ring1.fuse(1, ring2);
|
|
233
|
-
const code = decompile(fused);
|
|
359
|
+
const code = decompile(fused, { verbose: true });
|
|
234
360
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
235
361
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
236
362
|
export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9, atoms: [{ position: 0, depth: 0 }, { position: 1, depth: 0 }, { position: 6, depth: 0 }, { position: 7, depth: 0 }, { position: 8, depth: 0 }, { position: 9, depth: 0 }] }, { ring: v2, start: 1, end: 6, atoms: [{ position: 1, depth: 0 }, { position: 2, depth: 0 }, { position: 3, depth: 0 }, { position: 4, depth: 0 }, { position: 5, depth: 0 }, { position: 6, depth: 0 }] }] } });`);
|
|
@@ -241,7 +367,7 @@ export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9,
|
|
|
241
367
|
const ring2 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
242
368
|
const fused = ring1.fuse(1, ring2);
|
|
243
369
|
fused.metaLeadingBond = '=';
|
|
244
|
-
const code = decompile(fused);
|
|
370
|
+
const code = decompile(fused, { verbose: true });
|
|
245
371
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
246
372
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
247
373
|
export const v3 = FusedRing({ metadata: { leadingBond: '=', rings: [{ ring: v1, start: 0, end: 9, atoms: [{ position: 0, depth: 0 }, { position: 1, depth: 0 }, { position: 6, depth: 0 }, { position: 7, depth: 0 }, { position: 8, depth: 0 }, { position: 9, depth: 0 }] }, { ring: v2, start: 1, end: 6, atoms: [{ position: 1, depth: 0 }, { position: 2, depth: 0 }, { position: 3, depth: 0 }, { position: 4, depth: 0 }, { position: 5, depth: 0 }, { position: 6, depth: 0 }] }] } });`);
|
|
@@ -273,7 +399,7 @@ export const v3 = FusedRing({ metadata: { leadingBond: '=', rings: [{ ring: v1,
|
|
|
273
399
|
atoms: [{ position: 12, depth: 0, value: 'O' }],
|
|
274
400
|
},
|
|
275
401
|
});
|
|
276
|
-
const code = decompile(fused);
|
|
402
|
+
const code = decompile(fused, { verbose: true });
|
|
277
403
|
// Attachments stay as .attach() calls on rings.
|
|
278
404
|
// FusedRing uses hierarchical metadata with the extra atom (position 12, value 'O') colocated.
|
|
279
405
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
@@ -307,7 +433,7 @@ export const v5 = FusedRing({ metadata: { rings: [{ ring: v3, start: 0, end: 9,
|
|
|
307
433
|
atoms: [{ position: 12, depth: 0, value: 'N' }],
|
|
308
434
|
},
|
|
309
435
|
});
|
|
310
|
-
const code = decompile(fused);
|
|
436
|
+
const code = decompile(fused, { verbose: true });
|
|
311
437
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
312
438
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
313
439
|
export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9, atoms: [{ position: 0, depth: 0 }, { position: 1, depth: 0 }, { position: 6, depth: 0 }, { position: 7, depth: 0 }, { position: 8, depth: 0 }, { position: 9, depth: 0 }] }, { ring: v2, start: 1, end: 6, atoms: [{ position: 1, depth: 0 }, { position: 2, depth: 0 }, { position: 3, depth: 0 }, { position: 4, depth: 0 }, { position: 5, depth: 0 }, { position: 6, depth: 0 }] }], atoms: [{ position: 12, depth: 0, value: 'N' }] } });`);
|
|
@@ -318,14 +444,14 @@ export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9,
|
|
|
318
444
|
const fused = ring1.addSequentialRings([]);
|
|
319
445
|
fused.metaBranchDepthMap.set(5, 0);
|
|
320
446
|
fused.metaBondMap.set(5, null);
|
|
321
|
-
const code = decompile(fused, { includeMetadata: true });
|
|
447
|
+
const code = decompile(fused, { verbose: true, includeMetadata: true });
|
|
322
448
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
|
|
323
449
|
});
|
|
324
450
|
|
|
325
451
|
test('decompiles single-ring fused with empty maps returns just Ring', () => {
|
|
326
452
|
const ring1 = Ring({ atoms: 'C', size: 6 });
|
|
327
453
|
const fused = ring1.addSequentialRings([]);
|
|
328
|
-
const code = decompile(fused, { includeMetadata: true });
|
|
454
|
+
const code = decompile(fused, { verbose: true, includeMetadata: true });
|
|
329
455
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
|
|
330
456
|
});
|
|
331
457
|
|
|
@@ -335,12 +461,12 @@ export const v3 = FusedRing({ metadata: { rings: [{ ring: v1, start: 0, end: 9,
|
|
|
335
461
|
const fused = ring1.fuse(1, ring2);
|
|
336
462
|
fused.metaSequentialRings = [];
|
|
337
463
|
fused.rings = [ring1];
|
|
338
|
-
const code = decompile(fused);
|
|
464
|
+
const code = decompile(fused, { verbose: true });
|
|
339
465
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6 });");
|
|
340
466
|
});
|
|
341
467
|
});
|
|
342
468
|
|
|
343
|
-
describe('Decompiler - FusedRing (sequential rings)', () => {
|
|
469
|
+
describe('Decompiler - FusedRing (sequential rings, verbose)', () => {
|
|
344
470
|
// Sequential rings: all const, no let+reassignment.
|
|
345
471
|
// Depth colocated per-ring as { ring: v, depth: N }. Depth 0 is default (omitted).
|
|
346
472
|
// chainAtoms: depth always explicit.
|
|
@@ -351,7 +477,7 @@ describe('Decompiler - FusedRing (sequential rings)', () => {
|
|
|
351
477
|
const fused = ring1.fuse(1, ring2);
|
|
352
478
|
const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 3 });
|
|
353
479
|
const result = fused.addSequentialRings([seqRing]);
|
|
354
|
-
const code = decompile(result);
|
|
480
|
+
const code = decompile(result, { verbose: true });
|
|
355
481
|
// const for fuse, new const for addSequentialRings, depth 0 omitted
|
|
356
482
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
357
483
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
@@ -366,7 +492,7 @@ export const v5 = v3.addSequentialRings([{ ring: v4 }]);`);
|
|
|
366
492
|
const fused = ring1.fuse(1, ring2);
|
|
367
493
|
const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 3 });
|
|
368
494
|
const result = fused.addSequentialRings([seqRing]);
|
|
369
|
-
const code = decompile(result, { includeMetadata: true });
|
|
495
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
370
496
|
// depth 0 is default, so omitted in the ring entry
|
|
371
497
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
372
498
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
@@ -379,7 +505,7 @@ export const v5 = v3.addSequentialRings([{ ring: v4 }]);`);
|
|
|
379
505
|
const ring1 = Ring({ atoms: 'C', size: 6 });
|
|
380
506
|
const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
381
507
|
const result = ring1.addSequentialRings([seqRing]);
|
|
382
|
-
const code = decompile(result, { includeMetadata: true });
|
|
508
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
383
509
|
// Single base ring + sequential ring, all depth 0 (omitted), all const
|
|
384
510
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
385
511
|
export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
@@ -395,7 +521,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
|
395
521
|
atom: 'N', depth: 0, position: 'after', attachments: [attachment],
|
|
396
522
|
}],
|
|
397
523
|
});
|
|
398
|
-
const code = decompile(result, { includeMetadata: true });
|
|
524
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
399
525
|
// chainAtoms depth always explicit, attachment declared before addSequentialRings
|
|
400
526
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
401
527
|
export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
@@ -409,7 +535,7 @@ export const v4 = v1.addSequentialRings([{ ring: v2 }], { chainAtoms: [{ atom: '
|
|
|
409
535
|
const result = ring1.addSequentialRings([seqRing]);
|
|
410
536
|
// Position 10 is within the sequential ring — atom value here doesn't need chainAtom
|
|
411
537
|
result.metaAtomValueMap.set(10, 'N');
|
|
412
|
-
const code = decompile(result, { includeMetadata: true });
|
|
538
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
413
539
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
414
540
|
export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
415
541
|
export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
@@ -423,7 +549,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
|
423
549
|
result.metaBondMap.set(1, '=');
|
|
424
550
|
result.metaBondMap.set(2, null);
|
|
425
551
|
result.metaBondMap.set(3, '#');
|
|
426
|
-
const code = decompile(result, { includeMetadata: true });
|
|
552
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
427
553
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
428
554
|
export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
429
555
|
export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
@@ -437,7 +563,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
|
437
563
|
const result = ring1.addSequentialRings([seqRing]);
|
|
438
564
|
// Attachments at base ring position 3 don't generate chainAtoms
|
|
439
565
|
result.metaSeqAtomAttachments.set(3, [att1, att2]);
|
|
440
|
-
const code = decompile(result, { includeMetadata: true });
|
|
566
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
441
567
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
442
568
|
export const v2 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
443
569
|
export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
@@ -456,7 +582,7 @@ export const v3 = v1.addSequentialRings([{ ring: v2 }]);`);
|
|
|
456
582
|
const fused = FusedRing([ring1, ring2, ring3]);
|
|
457
583
|
const seqRing = Ring({ atoms: 'C', size: 5, ringNumber: 4 });
|
|
458
584
|
const result = fused.addSequentialRings([seqRing]);
|
|
459
|
-
const code = decompile(result, { includeMetadata: true });
|
|
585
|
+
const code = decompile(result, { verbose: true, includeMetadata: true });
|
|
460
586
|
// All const, new const for addSequentialRings, depth 0 omitted
|
|
461
587
|
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
462
588
|
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 3 });
|
|
@@ -467,12 +593,12 @@ export const v6 = v4.addSequentialRings([{ ring: v5 }]);`);
|
|
|
467
593
|
});
|
|
468
594
|
});
|
|
469
595
|
|
|
470
|
-
describe('Decompiler - Molecule', () => {
|
|
596
|
+
describe('Decompiler - Molecule (verbose)', () => {
|
|
471
597
|
test('decompiles molecule with multiple components', () => {
|
|
472
598
|
const propyl = Linear(['C', 'C', 'C']);
|
|
473
599
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
474
600
|
const molecule = Molecule([propyl, benzene]);
|
|
475
|
-
const code = decompile(molecule);
|
|
601
|
+
const code = decompile(molecule, { verbose: true });
|
|
476
602
|
expect(code).toBe(`export const v1 = Linear(['C', 'C', 'C']);
|
|
477
603
|
export const v2 = Ring({ atoms: 'c', size: 6 });
|
|
478
604
|
export const v3 = Molecule([v1, v2]);`);
|
|
@@ -480,38 +606,66 @@ export const v3 = Molecule([v1, v2]);`);
|
|
|
480
606
|
|
|
481
607
|
test('decompiles empty molecule', () => {
|
|
482
608
|
const empty = Molecule([]);
|
|
483
|
-
const code = decompile(empty);
|
|
609
|
+
const code = decompile(empty, { verbose: true });
|
|
484
610
|
expect(code).toBe('export const v1 = Molecule([]);');
|
|
485
611
|
});
|
|
486
612
|
|
|
487
|
-
test('uses toCode() method', () => {
|
|
613
|
+
test('uses toCode() method with verbose', () => {
|
|
488
614
|
const propyl = Linear(['C', 'C', 'C']);
|
|
489
615
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
490
616
|
const molecule = Molecule([propyl, benzene]);
|
|
491
|
-
const code = molecule.toCode();
|
|
617
|
+
const code = molecule.toCode('molecule', { verbose: true });
|
|
492
618
|
expect(code).toBe(`export const molecule1 = Linear(['C', 'C', 'C']);
|
|
493
619
|
export const molecule2 = Ring({ atoms: 'c', size: 6 });
|
|
494
620
|
export const molecule3 = Molecule([molecule1, molecule2]);`);
|
|
495
621
|
});
|
|
496
622
|
});
|
|
497
623
|
|
|
624
|
+
describe('Decompiler - Molecule (non-verbose)', () => {
|
|
625
|
+
test('decompiles molecule with Fragment', () => {
|
|
626
|
+
const propyl = Linear(['C', 'C', 'C']);
|
|
627
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
628
|
+
const molecule = Molecule([propyl, benzene]);
|
|
629
|
+
const code = decompile(molecule);
|
|
630
|
+
expect(code).toBe(`export const v1 = Fragment('CCC');
|
|
631
|
+
export const v2 = Fragment('c1ccccc1');
|
|
632
|
+
export const v3 = Molecule([v1, v2]);`);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('decompiles empty molecule same as verbose', () => {
|
|
636
|
+
const empty = Molecule([]);
|
|
637
|
+
const code = decompile(empty);
|
|
638
|
+
expect(code).toBe('export const v1 = Molecule([]);');
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test('uses toCode() non-verbose by default', () => {
|
|
642
|
+
const propyl = Linear(['C', 'C', 'C']);
|
|
643
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
644
|
+
const molecule = Molecule([propyl, benzene]);
|
|
645
|
+
const code = molecule.toCode();
|
|
646
|
+
expect(code).toBe(`export const molecule1 = Fragment('CCC');
|
|
647
|
+
export const molecule2 = Fragment('c1ccccc1');
|
|
648
|
+
export const molecule3 = Molecule([molecule1, molecule2]);`);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
498
652
|
describe('Decompiler - Options', () => {
|
|
499
653
|
test('handles custom indent option', () => {
|
|
500
654
|
const ring = Ring({ atoms: 'C', size: 6 });
|
|
501
|
-
const code = decompile(ring, { indent: 2 });
|
|
655
|
+
const code = decompile(ring, { indent: 2, verbose: true });
|
|
502
656
|
expect(code).toBe(" export const v1 = Ring({ atoms: 'C', size: 6 });");
|
|
503
657
|
});
|
|
504
658
|
|
|
505
659
|
test('handles custom varName option', () => {
|
|
506
660
|
const ring = Ring({ atoms: 'C', size: 6 });
|
|
507
|
-
const code = decompile(ring, { varName: 'node' });
|
|
661
|
+
const code = decompile(ring, { varName: 'node', verbose: true });
|
|
508
662
|
expect(code).toBe("export const node1 = Ring({ atoms: 'C', size: 6 });");
|
|
509
663
|
});
|
|
510
664
|
|
|
511
665
|
test('includeMetadata false strips branchDepths', () => {
|
|
512
666
|
const ring = Ring({ atoms: 'C', size: 6 });
|
|
513
667
|
ring.metaBranchDepths = [0, 0, 0, 1, 1, 1];
|
|
514
|
-
const code = decompile(ring, { includeMetadata: false });
|
|
668
|
+
const code = decompile(ring, { verbose: true, includeMetadata: false });
|
|
515
669
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, branchDepths: [0, 0, 0, 1, 1, 1] });");
|
|
516
670
|
});
|
|
517
671
|
|
|
@@ -522,21 +676,39 @@ describe('Decompiler - Options', () => {
|
|
|
522
676
|
});
|
|
523
677
|
|
|
524
678
|
describe('Decompiler - Round-trip', () => {
|
|
525
|
-
test('generated code can be evaluated', () => {
|
|
679
|
+
test('generated code can be evaluated (verbose)', () => {
|
|
526
680
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
527
|
-
const code = benzene.toCode('r');
|
|
681
|
+
const code = benzene.toCode('r', { verbose: true });
|
|
528
682
|
expect(code).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
529
683
|
});
|
|
530
684
|
|
|
531
|
-
test('
|
|
685
|
+
test('generated code can be evaluated (non-verbose)', () => {
|
|
686
|
+
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
687
|
+
const code = benzene.toCode('r');
|
|
688
|
+
expect(code).toBe("export const r1 = Fragment('c1ccccc1');");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test('preserves structure through decompile (verbose)', () => {
|
|
532
692
|
const propane = Linear(['C', 'C', 'C']);
|
|
533
|
-
const code = propane.toCode('p');
|
|
693
|
+
const code = propane.toCode('p', { verbose: true });
|
|
534
694
|
expect(code).toBe("export const p1 = Linear(['C', 'C', 'C']);");
|
|
535
695
|
});
|
|
536
696
|
|
|
537
|
-
test('
|
|
697
|
+
test('preserves structure through decompile (non-verbose)', () => {
|
|
698
|
+
const propane = Linear(['C', 'C', 'C']);
|
|
699
|
+
const code = propane.toCode('p');
|
|
700
|
+
expect(code).toBe("export const p1 = Fragment('CCC');");
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test('round-trips parsed ring with bonds (verbose)', () => {
|
|
538
704
|
const ast = parse('C1CC=CC1');
|
|
539
|
-
const code = decompile(ast, { includeMetadata: true });
|
|
705
|
+
const code = decompile(ast, { verbose: true, includeMetadata: true });
|
|
540
706
|
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 5, bonds: [null, null, '=', null, null] });");
|
|
541
707
|
});
|
|
708
|
+
|
|
709
|
+
test('round-trips parsed ring with bonds (non-verbose)', () => {
|
|
710
|
+
const ast = parse('C1CC=CC1');
|
|
711
|
+
const code = decompile(ast, { includeMetadata: true });
|
|
712
|
+
expect(code).toBe("export const v1 = Fragment('C1CC=CC1');");
|
|
713
|
+
});
|
|
542
714
|
});
|
package/src/fragment.test.js
CHANGED
|
@@ -61,8 +61,13 @@ describe('Fragment', () => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
test('toCode works on fragment', () => {
|
|
64
|
+
test('toCode works on fragment (non-verbose default)', () => {
|
|
65
65
|
const benzene = Fragment('c1ccccc1');
|
|
66
|
-
expect(benzene.toCode('v')).toBe("export const v1 =
|
|
66
|
+
expect(benzene.toCode('v')).toBe("export const v1 = Fragment('c1ccccc1');");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('toCode works on fragment (verbose)', () => {
|
|
70
|
+
const benzene = Fragment('c1ccccc1');
|
|
71
|
+
expect(benzene.toCode('v', { verbose: true })).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
|
|
67
72
|
});
|
|
68
73
|
});
|