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
|
@@ -76,14 +76,35 @@ export function buildBranchCrossingRingSMILES(ring, buildSMILES) {
|
|
|
76
76
|
|
|
77
77
|
if (attachments[i] && attachments[i].length > 0) {
|
|
78
78
|
if (hasInlineBranchAfter) {
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// Determine the branchId of the inline continuation (next ring position)
|
|
80
|
+
const metaBranchIds = ring.metaBranchIds || [];
|
|
81
|
+
const inlineBranchId = i < size ? (metaBranchIds[i] || Infinity) : Infinity;
|
|
82
|
+
|
|
83
|
+
// Separate: siblings that come BEFORE the inline branch (emit now)
|
|
84
|
+
// vs siblings that come AFTER (delay), and non-siblings (delay)
|
|
85
|
+
const emitNow = [];
|
|
86
|
+
const delay = [];
|
|
87
|
+
attachments[i].forEach((att) => {
|
|
88
|
+
const isSibling = att.metaIsSibling !== undefined ? att.metaIsSibling : true;
|
|
89
|
+
if (!isSibling) {
|
|
90
|
+
delay.push(att);
|
|
91
|
+
} else if (att.metaBranchId !== undefined && att.metaBranchId < inlineBranchId) {
|
|
92
|
+
emitNow.push(att);
|
|
93
|
+
} else if (att.metaBeforeInline === true) {
|
|
94
|
+
emitNow.push(att);
|
|
95
|
+
} else {
|
|
96
|
+
// Default: delay sibling (beforeInline defaults to false)
|
|
97
|
+
delay.push({ ...att, metaIsSibling: true });
|
|
83
98
|
}
|
|
84
|
-
return { ...att, metaIsSibling: true };
|
|
85
99
|
});
|
|
86
|
-
|
|
100
|
+
// Emit siblings that come before the inline branch
|
|
101
|
+
emitNow.forEach((attachment) => {
|
|
102
|
+
emitAttachment(parts, attachment, buildSMILES);
|
|
103
|
+
});
|
|
104
|
+
// Delay the rest
|
|
105
|
+
if (delay.length > 0) {
|
|
106
|
+
pendingAttachments.set(i, { depth: posDepth, attachments: delay });
|
|
107
|
+
}
|
|
87
108
|
} else {
|
|
88
109
|
// Output attachments immediately
|
|
89
110
|
attachments[i].forEach((attachment) => {
|
|
@@ -103,11 +103,33 @@ export function buildInterleavedFusedRingSMILES(fusedRing, buildSMILES) {
|
|
|
103
103
|
// Pending attachments: depth -> attachment[]
|
|
104
104
|
const pendingAttachments = new Map();
|
|
105
105
|
|
|
106
|
+
const branchIdMap = fusedRing.metaBranchIdMap || new Map();
|
|
107
|
+
let prevBranchId = null;
|
|
108
|
+
|
|
106
109
|
allPositions.forEach((pos, idx) => {
|
|
107
110
|
const entry = atomSequence[pos];
|
|
108
111
|
if (!entry) return;
|
|
109
112
|
|
|
110
113
|
const posDepth = branchDepthMap.get(pos) || 0;
|
|
114
|
+
const posBranchId = branchIdMap.get(pos);
|
|
115
|
+
|
|
116
|
+
// Detect sibling branch switch: same depth, different branch ID
|
|
117
|
+
// Need to close current branch and reopen a new one
|
|
118
|
+
if (posDepth > 0 && posDepth === depthRef.value && prevBranchId !== null
|
|
119
|
+
&& posBranchId !== prevBranchId && posBranchId !== null) {
|
|
120
|
+
// Close the current branch and reopen
|
|
121
|
+
parts.push(')');
|
|
122
|
+
// Emit any pending attachments at the parent depth
|
|
123
|
+
const parentDepth = posDepth - 1;
|
|
124
|
+
if (pendingAttachments.has(parentDepth)) {
|
|
125
|
+
const attachmentsToOutput = pendingAttachments.get(parentDepth);
|
|
126
|
+
attachmentsToOutput.forEach((attachment) => {
|
|
127
|
+
emitAttachment(parts, attachment, buildSMILES);
|
|
128
|
+
});
|
|
129
|
+
pendingAttachments.delete(parentDepth);
|
|
130
|
+
}
|
|
131
|
+
parts.push('(');
|
|
132
|
+
}
|
|
111
133
|
|
|
112
134
|
// Handle branch depth changes
|
|
113
135
|
openBranches(parts, depthRef, posDepth);
|
|
@@ -164,6 +186,8 @@ export function buildInterleavedFusedRingSMILES(fusedRing, buildSMILES) {
|
|
|
164
186
|
});
|
|
165
187
|
}
|
|
166
188
|
}
|
|
189
|
+
|
|
190
|
+
prevBranchId = posBranchId;
|
|
167
191
|
});
|
|
168
192
|
|
|
169
193
|
// Close any remaining open branches
|
package/src/decompiler.js
CHANGED
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
isRingNode,
|
|
10
10
|
isLinearNode,
|
|
11
11
|
} from './ast.js';
|
|
12
|
+
import { buildSMILES } from './codegen/index.js';
|
|
13
|
+
import { createRingNode } from './node-creators.js';
|
|
12
14
|
|
|
13
15
|
// Helper to call decompileNode (satisfies no-loop-func rule)
|
|
14
|
-
function decompileChildNode(node, indent, nextVar) {
|
|
16
|
+
function decompileChildNode(node, indent, nextVar, verbose) {
|
|
15
17
|
// eslint-disable-next-line no-use-before-define
|
|
16
|
-
return decompileNode(node, indent, nextVar);
|
|
18
|
+
return decompileNode(node, indent, nextVar, verbose);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -110,22 +112,43 @@ function generateSubstitutionCode(ring, indent, nextVar, initialVar) {
|
|
|
110
112
|
* Generate code for ring attachments
|
|
111
113
|
* @returns {{ lines: string[], currentVar: string }}
|
|
112
114
|
*/
|
|
113
|
-
function generateAttachmentCode(ring, indent, nextVar, initialVar) {
|
|
115
|
+
function generateAttachmentCode(ring, indent, nextVar, initialVar, verbose = true) {
|
|
114
116
|
const lines = [];
|
|
115
117
|
let currentVar = initialVar;
|
|
116
118
|
|
|
117
119
|
if (ring.attachments && Object.keys(ring.attachments).length > 0) {
|
|
120
|
+
// Determine inline branchId for sibling ordering
|
|
121
|
+
const metaBranchIds = ring.metaBranchIds || [];
|
|
122
|
+
const normalizedDepths = ring.metaBranchDepths || [];
|
|
123
|
+
|
|
118
124
|
Object.entries(ring.attachments).forEach(([pos, attachmentList]) => {
|
|
125
|
+
const posIdx = Number(pos) - 1; // 0-indexed
|
|
126
|
+
const nextIdx = posIdx + 1;
|
|
127
|
+
const posDepth = normalizedDepths[posIdx] || 0;
|
|
128
|
+
const nextDepth = nextIdx < normalizedDepths.length ? (normalizedDepths[nextIdx] || 0) : 0;
|
|
129
|
+
const hasInlineBranchAfter = nextDepth > posDepth;
|
|
130
|
+
const inlineBranchId = hasInlineBranchAfter ? (metaBranchIds[nextIdx] || Infinity) : Infinity;
|
|
131
|
+
|
|
119
132
|
attachmentList.forEach((attachment) => {
|
|
120
|
-
const attachResult = decompileChildNode(attachment, indent, nextVar);
|
|
133
|
+
const attachResult = decompileChildNode(attachment, indent, nextVar, verbose);
|
|
121
134
|
lines.push(attachResult.code);
|
|
122
135
|
|
|
123
136
|
const newVar = nextVar();
|
|
124
137
|
const isSibling = attachment.metaIsSibling;
|
|
138
|
+
const optParts = [];
|
|
125
139
|
if (isSibling === true) {
|
|
126
|
-
|
|
140
|
+
optParts.push('sibling: true');
|
|
141
|
+
// Encode beforeInline for sibling ordering in branch-crossing rings
|
|
142
|
+
// Only emit when true since false is the default
|
|
143
|
+
if (hasInlineBranchAfter && attachment.metaBranchId !== undefined
|
|
144
|
+
&& attachment.metaBranchId < inlineBranchId) {
|
|
145
|
+
optParts.push('beforeInline: true');
|
|
146
|
+
}
|
|
127
147
|
} else if (isSibling === false) {
|
|
128
|
-
|
|
148
|
+
optParts.push('sibling: false');
|
|
149
|
+
}
|
|
150
|
+
if (optParts.length > 0) {
|
|
151
|
+
lines.push(`${indent}const ${newVar} = ${currentVar}.attach(${pos}, ${attachResult.finalVar}, { ${optParts.join(', ')} });`);
|
|
129
152
|
} else {
|
|
130
153
|
lines.push(`${indent}const ${newVar} = ${currentVar}.attach(${pos}, ${attachResult.finalVar});`);
|
|
131
154
|
}
|
|
@@ -137,13 +160,72 @@ function generateAttachmentCode(ring, indent, nextVar, initialVar) {
|
|
|
137
160
|
return { lines, currentVar };
|
|
138
161
|
}
|
|
139
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Compute the SMILES for a ring node with substitutions but without attachments.
|
|
165
|
+
* Used in non-verbose mode to emit Fragment('SMILES') for the substituted ring.
|
|
166
|
+
*/
|
|
167
|
+
function getRingWithSubsSmiles(ring) {
|
|
168
|
+
const tempNode = createRingNode(
|
|
169
|
+
ring.atoms,
|
|
170
|
+
ring.size,
|
|
171
|
+
ring.ringNumber,
|
|
172
|
+
ring.offset,
|
|
173
|
+
ring.substitutions,
|
|
174
|
+
{},
|
|
175
|
+
ring.bonds,
|
|
176
|
+
ring.metaBranchDepths,
|
|
177
|
+
);
|
|
178
|
+
if (ring.metaLeadingBond) {
|
|
179
|
+
tempNode.metaLeadingBond = ring.metaLeadingBond;
|
|
180
|
+
}
|
|
181
|
+
return buildSMILES(tempNode);
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
/**
|
|
141
185
|
* Decompile a Ring node
|
|
142
186
|
*/
|
|
143
|
-
function decompileRing(ring, indent, nextVar) {
|
|
187
|
+
function decompileRing(ring, indent, nextVar, verbose = true) {
|
|
144
188
|
const lines = [];
|
|
145
189
|
const varName = nextVar();
|
|
146
190
|
|
|
191
|
+
if (!verbose && !ring.metaLeadingBond) {
|
|
192
|
+
// Non-verbose: emit Fragment('SMILES') for base ring
|
|
193
|
+
const baseSmiles = buildSMILES(createRingNode(
|
|
194
|
+
ring.atoms,
|
|
195
|
+
ring.size,
|
|
196
|
+
ring.ringNumber,
|
|
197
|
+
ring.offset,
|
|
198
|
+
{},
|
|
199
|
+
{},
|
|
200
|
+
ring.bonds,
|
|
201
|
+
ring.metaBranchDepths,
|
|
202
|
+
));
|
|
203
|
+
lines.push(`${indent}const ${varName} = Fragment('${baseSmiles}');`);
|
|
204
|
+
|
|
205
|
+
// Substitutions: each becomes an independent Fragment('SMILES')
|
|
206
|
+
let currentVar = varName;
|
|
207
|
+
if (Object.keys(ring.substitutions).length > 0) {
|
|
208
|
+
const subsSmiles = getRingWithSubsSmiles(ring);
|
|
209
|
+
const newVar = nextVar();
|
|
210
|
+
lines.push(`${indent}const ${newVar} = Fragment('${subsSmiles}');`);
|
|
211
|
+
currentVar = newVar;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Attachments stay as .attach() calls
|
|
215
|
+
const { lines: attLines, currentVar: attVar } = generateAttachmentCode(
|
|
216
|
+
ring,
|
|
217
|
+
indent,
|
|
218
|
+
nextVar,
|
|
219
|
+
currentVar,
|
|
220
|
+
verbose,
|
|
221
|
+
);
|
|
222
|
+
lines.push(...attLines);
|
|
223
|
+
currentVar = attVar;
|
|
224
|
+
|
|
225
|
+
return { code: lines.join('\n'), finalVar: currentVar };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Verbose mode (original behavior)
|
|
147
229
|
// Build options object (include branchDepths for full decompilation)
|
|
148
230
|
const { optionsStr } = buildRingOptions(ring, { includeBranchDepths: true });
|
|
149
231
|
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
@@ -164,6 +246,7 @@ function decompileRing(ring, indent, nextVar) {
|
|
|
164
246
|
indent,
|
|
165
247
|
nextVar,
|
|
166
248
|
currentVar,
|
|
249
|
+
verbose,
|
|
167
250
|
);
|
|
168
251
|
lines.push(...attLines);
|
|
169
252
|
currentVar = attVar;
|
|
@@ -174,7 +257,7 @@ function decompileRing(ring, indent, nextVar) {
|
|
|
174
257
|
/**
|
|
175
258
|
* Decompile a Linear node
|
|
176
259
|
*/
|
|
177
|
-
function decompileLinear(linear, indent, nextVar) {
|
|
260
|
+
function decompileLinear(linear, indent, nextVar, verbose = true) {
|
|
178
261
|
const lines = [];
|
|
179
262
|
const varName = nextVar();
|
|
180
263
|
|
|
@@ -184,7 +267,13 @@ function decompileLinear(linear, indent, nextVar) {
|
|
|
184
267
|
const hasNonNullBonds = linear.bonds.some((b) => b !== null);
|
|
185
268
|
const hasLeadingBond = linear.metaLeadingBond !== undefined;
|
|
186
269
|
|
|
187
|
-
|
|
270
|
+
// Non-verbose: use Fragment('SMILES') when possible (no bonds, no leadingBond)
|
|
271
|
+
if (!verbose && !hasNonNullBonds && !hasLeadingBond) {
|
|
272
|
+
// Compute SMILES without attachments for the base linear node
|
|
273
|
+
const baseLinear = { ...linear, attachments: {} };
|
|
274
|
+
const smiles = buildSMILES(baseLinear);
|
|
275
|
+
lines.push(`${indent}const ${varName} = Fragment('${smiles}');`);
|
|
276
|
+
} else if (hasNonNullBonds && hasLeadingBond) {
|
|
188
277
|
lines.push(`${indent}const ${varName} = Linear([${atomsStr}], [${formatBondsArray(linear.bonds)}], {}, '${linear.metaLeadingBond}');`);
|
|
189
278
|
} else if (hasNonNullBonds) {
|
|
190
279
|
lines.push(`${indent}const ${varName} = Linear([${atomsStr}], [${formatBondsArray(linear.bonds)}]);`);
|
|
@@ -201,7 +290,7 @@ function decompileLinear(linear, indent, nextVar) {
|
|
|
201
290
|
Object.entries(linear.attachments).forEach(([pos, attachmentList]) => {
|
|
202
291
|
attachmentList.forEach((attachment) => {
|
|
203
292
|
// eslint-disable-next-line no-use-before-define
|
|
204
|
-
const attachRes = decompileNode(attachment, indent, nextVar);
|
|
293
|
+
const attachRes = decompileNode(attachment, indent, nextVar, verbose);
|
|
205
294
|
const { code: aCode, finalVar: aFinalVar } = attachRes;
|
|
206
295
|
lines.push(aCode);
|
|
207
296
|
|
|
@@ -402,7 +491,7 @@ function computeSharedPositions(fusedRing) {
|
|
|
402
491
|
* Emits .fuse() for the first pair and .addRing() for subsequent rings.
|
|
403
492
|
* The resulting code goes through the simple codegen path (offset-based).
|
|
404
493
|
*/
|
|
405
|
-
function decompileSimpleFusedRing(fusedRing, indent, nextVar) {
|
|
494
|
+
function decompileSimpleFusedRing(fusedRing, indent, nextVar, verbose = true) {
|
|
406
495
|
const lines = [];
|
|
407
496
|
const ringFinalVars = [];
|
|
408
497
|
|
|
@@ -465,20 +554,52 @@ function decompileSimpleFusedRing(fusedRing, indent, nextVar) {
|
|
|
465
554
|
}
|
|
466
555
|
|
|
467
556
|
const varName = nextVar();
|
|
468
|
-
const { optionsStr } = buildRingOptions(effectiveRing, { includeBranchDepths: true });
|
|
469
|
-
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
470
557
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
558
|
+
if (!verbose && !effectiveRing.metaLeadingBond) {
|
|
559
|
+
// Non-verbose: use Fragment for ring constructor
|
|
560
|
+
const baseSmiles = buildSMILES(createRingNode(
|
|
561
|
+
effectiveRing.atoms,
|
|
562
|
+
effectiveRing.size,
|
|
563
|
+
effectiveRing.ringNumber,
|
|
564
|
+
effectiveRing.offset,
|
|
565
|
+
{},
|
|
566
|
+
{},
|
|
567
|
+
effectiveRing.bonds,
|
|
568
|
+
effectiveRing.metaBranchDepths,
|
|
569
|
+
));
|
|
570
|
+
lines.push(`${indent}const ${varName} = Fragment('${baseSmiles}');`);
|
|
571
|
+
|
|
572
|
+
let currentVar = varName;
|
|
573
|
+
if (Object.keys(effectiveRing.substitutions || {}).length > 0) {
|
|
574
|
+
const subsSmiles = getRingWithSubsSmiles(effectiveRing);
|
|
575
|
+
const newVar = nextVar();
|
|
576
|
+
lines.push(`${indent}const ${newVar} = Fragment('${subsSmiles}');`);
|
|
577
|
+
currentVar = newVar;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const {
|
|
581
|
+
lines: attLines, currentVar: attVar,
|
|
582
|
+
} = generateAttachmentCode(effectiveRing, indent, nextVar, currentVar, verbose);
|
|
583
|
+
lines.push(...attLines);
|
|
475
584
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
585
|
+
ringFinalVars.push(attVar);
|
|
586
|
+
} else {
|
|
587
|
+
// Verbose: original behavior
|
|
588
|
+
const { optionsStr } = buildRingOptions(effectiveRing, { includeBranchDepths: true });
|
|
589
|
+
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
590
|
+
|
|
591
|
+
const {
|
|
592
|
+
lines: subLines, currentVar: subVar,
|
|
593
|
+
} = generateSubstitutionCode(effectiveRing, indent, nextVar, varName);
|
|
594
|
+
lines.push(...subLines);
|
|
595
|
+
|
|
596
|
+
const {
|
|
597
|
+
lines: attLines, currentVar: attVar,
|
|
598
|
+
} = generateAttachmentCode(effectiveRing, indent, nextVar, subVar, verbose);
|
|
599
|
+
lines.push(...attLines);
|
|
480
600
|
|
|
481
|
-
|
|
601
|
+
ringFinalVars.push(attVar);
|
|
602
|
+
}
|
|
482
603
|
});
|
|
483
604
|
|
|
484
605
|
const leadingBond = fusedRing.metaLeadingBond;
|
|
@@ -520,7 +641,7 @@ function decompileSimpleFusedRing(fusedRing, indent, nextVar) {
|
|
|
520
641
|
* rings to a fused ring. The codegen uses this metadata to correctly interleave
|
|
521
642
|
* ring markers and handle branch depths.
|
|
522
643
|
*/
|
|
523
|
-
function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
644
|
+
function decompileComplexFusedRing(fusedRing, indent, nextVar, verbose = true) {
|
|
524
645
|
const lines = [];
|
|
525
646
|
const sequentialRings = fusedRing.metaSequentialRings || [];
|
|
526
647
|
const seqAtomAttachments = fusedRing.metaSeqAtomAttachments || new Map();
|
|
@@ -528,17 +649,45 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
528
649
|
// Step 1: Decompile the base fused ring
|
|
529
650
|
const ringVars = [];
|
|
530
651
|
fusedRing.rings.forEach((ring) => {
|
|
531
|
-
const { optionsStr } = buildRingOptions(ring, { includeBranchDepths: true });
|
|
532
652
|
const varName = nextVar();
|
|
533
|
-
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
534
653
|
|
|
535
|
-
|
|
536
|
-
|
|
654
|
+
if (!verbose && !ring.metaLeadingBond) {
|
|
655
|
+
const baseSmiles = buildSMILES(createRingNode(
|
|
656
|
+
ring.atoms,
|
|
657
|
+
ring.size,
|
|
658
|
+
ring.ringNumber,
|
|
659
|
+
ring.offset,
|
|
660
|
+
{},
|
|
661
|
+
{},
|
|
662
|
+
ring.bonds,
|
|
663
|
+
ring.metaBranchDepths,
|
|
664
|
+
));
|
|
665
|
+
lines.push(`${indent}const ${varName} = Fragment('${baseSmiles}');`);
|
|
666
|
+
|
|
667
|
+
let currentVar = varName;
|
|
668
|
+
if (Object.keys(ring.substitutions || {}).length > 0) {
|
|
669
|
+
const subsSmiles = getRingWithSubsSmiles(ring);
|
|
670
|
+
const newVar = nextVar();
|
|
671
|
+
lines.push(`${indent}const ${newVar} = Fragment('${subsSmiles}');`);
|
|
672
|
+
currentVar = newVar;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const attResult = generateAttachmentCode(ring, indent, nextVar, currentVar, verbose);
|
|
676
|
+
lines.push(...attResult.lines);
|
|
677
|
+
|
|
678
|
+
ringVars.push({ var: attResult.currentVar, ring });
|
|
679
|
+
} else {
|
|
680
|
+
const { optionsStr } = buildRingOptions(ring, { includeBranchDepths: true });
|
|
681
|
+
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
537
682
|
|
|
538
|
-
|
|
539
|
-
|
|
683
|
+
const subResult = generateSubstitutionCode(ring, indent, nextVar, varName);
|
|
684
|
+
lines.push(...subResult.lines);
|
|
685
|
+
|
|
686
|
+
const attResult = generateAttachmentCode(ring, indent, nextVar, subResult.currentVar, verbose);
|
|
687
|
+
lines.push(...attResult.lines);
|
|
540
688
|
|
|
541
|
-
|
|
689
|
+
ringVars.push({ var: attResult.currentVar, ring });
|
|
690
|
+
}
|
|
542
691
|
});
|
|
543
692
|
|
|
544
693
|
// Decompile seqAtomAttachments BEFORE fuse so their vars are declared early
|
|
@@ -548,7 +697,7 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
548
697
|
seqAtomAttachments.forEach((attachments, pos) => {
|
|
549
698
|
const attVars = [];
|
|
550
699
|
attachments.forEach((att) => {
|
|
551
|
-
const attResult = decompileChildNode(att, indent, nextVar);
|
|
700
|
+
const attResult = decompileChildNode(att, indent, nextVar, verbose);
|
|
552
701
|
lines.push(attResult.code);
|
|
553
702
|
attVars.push(attResult.finalVar);
|
|
554
703
|
});
|
|
@@ -670,17 +819,45 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
670
819
|
// Decompile sequential rings
|
|
671
820
|
const seqRingVars = [];
|
|
672
821
|
sequentialRings.forEach((ring) => {
|
|
673
|
-
const { optionsStr } = buildRingOptions(ring, { includeBranchDepths: true });
|
|
674
822
|
const varName = nextVar();
|
|
675
|
-
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
676
823
|
|
|
677
|
-
|
|
678
|
-
|
|
824
|
+
if (!verbose && !ring.metaLeadingBond) {
|
|
825
|
+
const baseSmiles = buildSMILES(createRingNode(
|
|
826
|
+
ring.atoms,
|
|
827
|
+
ring.size,
|
|
828
|
+
ring.ringNumber,
|
|
829
|
+
ring.offset,
|
|
830
|
+
{},
|
|
831
|
+
{},
|
|
832
|
+
ring.bonds,
|
|
833
|
+
ring.metaBranchDepths,
|
|
834
|
+
));
|
|
835
|
+
lines.push(`${indent}const ${varName} = Fragment('${baseSmiles}');`);
|
|
836
|
+
|
|
837
|
+
let currentVar = varName;
|
|
838
|
+
if (Object.keys(ring.substitutions || {}).length > 0) {
|
|
839
|
+
const subsSmiles = getRingWithSubsSmiles(ring);
|
|
840
|
+
const newVar = nextVar();
|
|
841
|
+
lines.push(`${indent}const ${newVar} = Fragment('${subsSmiles}');`);
|
|
842
|
+
currentVar = newVar;
|
|
843
|
+
}
|
|
679
844
|
|
|
680
|
-
|
|
681
|
-
|
|
845
|
+
const attResult = generateAttachmentCode(ring, indent, nextVar, currentVar, verbose);
|
|
846
|
+
lines.push(...attResult.lines);
|
|
682
847
|
|
|
683
|
-
|
|
848
|
+
seqRingVars.push(attResult.currentVar);
|
|
849
|
+
} else {
|
|
850
|
+
const { optionsStr } = buildRingOptions(ring, { includeBranchDepths: true });
|
|
851
|
+
lines.push(`${indent}const ${varName} = Ring({ ${optionsStr} });`);
|
|
852
|
+
|
|
853
|
+
const subResult = generateSubstitutionCode(ring, indent, nextVar, varName);
|
|
854
|
+
lines.push(...subResult.lines);
|
|
855
|
+
|
|
856
|
+
const attResult = generateAttachmentCode(ring, indent, nextVar, subResult.currentVar, verbose);
|
|
857
|
+
lines.push(...attResult.lines);
|
|
858
|
+
|
|
859
|
+
seqRingVars.push(attResult.currentVar);
|
|
860
|
+
}
|
|
684
861
|
});
|
|
685
862
|
|
|
686
863
|
// Decompile chain atom attachments
|
|
@@ -689,7 +866,7 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
689
866
|
const attachments = seqAtomAttachments.get(entry.attachmentPos) || [];
|
|
690
867
|
const attVars = [];
|
|
691
868
|
attachments.forEach((att) => {
|
|
692
|
-
const attResult = decompileChildNode(att, indent, nextVar);
|
|
869
|
+
const attResult = decompileChildNode(att, indent, nextVar, verbose);
|
|
693
870
|
lines.push(attResult.code);
|
|
694
871
|
attVars.push(attResult.finalVar);
|
|
695
872
|
});
|
|
@@ -758,6 +935,7 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
758
935
|
const atomValueMap = fusedRing.metaAtomValueMap || new Map();
|
|
759
936
|
const bondMap = fusedRing.metaBondMap || new Map();
|
|
760
937
|
const ringOrderMap = fusedRing.metaRingOrderMap;
|
|
938
|
+
const branchIdMap = fusedRing.metaBranchIdMap || new Map();
|
|
761
939
|
|
|
762
940
|
// Helper to format a single atom entry with all available metadata
|
|
763
941
|
function formatAtomEntry(pos) {
|
|
@@ -768,6 +946,9 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
768
946
|
if (ringOrderMap && ringOrderMap.has(pos)) {
|
|
769
947
|
parts.push(`rings: [${ringOrderMap.get(pos).join(', ')}]`);
|
|
770
948
|
}
|
|
949
|
+
if (branchIdMap.has(pos) && branchIdMap.get(pos) !== null) {
|
|
950
|
+
parts.push(`branchId: ${branchIdMap.get(pos)}`);
|
|
951
|
+
}
|
|
771
952
|
if (seqAtomAttachmentVarMap.has(pos)) {
|
|
772
953
|
parts.push(`attachments: [${seqAtomAttachmentVarMap.get(pos).join(', ')}]`);
|
|
773
954
|
}
|
|
@@ -820,24 +1001,24 @@ function decompileComplexFusedRing(fusedRing, indent, nextVar) {
|
|
|
820
1001
|
* - Sequential rings or interleaved codegen → preserves metadata (needs it for correct SMILES)
|
|
821
1002
|
* - Everything else → structural API calls only (no metadata)
|
|
822
1003
|
*/
|
|
823
|
-
function decompileFusedRing(fusedRing, indent, nextVar) {
|
|
1004
|
+
function decompileFusedRing(fusedRing, indent, nextVar, verbose = true) {
|
|
824
1005
|
const seqRings = fusedRing.metaSequentialRings;
|
|
825
1006
|
const hasSeqRings = seqRings && seqRings.length > 0;
|
|
826
1007
|
const isInterleaved = needsInterleavedCodegen(fusedRing);
|
|
827
1008
|
|
|
828
1009
|
// Use complex decompilation for sequential rings or genuinely interleaved fused rings
|
|
829
1010
|
if (hasSeqRings || isInterleaved) {
|
|
830
|
-
return decompileComplexFusedRing(fusedRing, indent, nextVar);
|
|
1011
|
+
return decompileComplexFusedRing(fusedRing, indent, nextVar, verbose);
|
|
831
1012
|
}
|
|
832
1013
|
|
|
833
1014
|
// Everything else goes through the simple path (no metadata)
|
|
834
|
-
return decompileSimpleFusedRing(fusedRing, indent, nextVar);
|
|
1015
|
+
return decompileSimpleFusedRing(fusedRing, indent, nextVar, verbose);
|
|
835
1016
|
}
|
|
836
1017
|
|
|
837
1018
|
/**
|
|
838
1019
|
* Decompile a Molecule node
|
|
839
1020
|
*/
|
|
840
|
-
function decompileMolecule(molecule, indent, nextVar) {
|
|
1021
|
+
function decompileMolecule(molecule, indent, nextVar, verbose = true) {
|
|
841
1022
|
const lines = [];
|
|
842
1023
|
const { components } = molecule;
|
|
843
1024
|
|
|
@@ -853,7 +1034,7 @@ function decompileMolecule(molecule, indent, nextVar) {
|
|
|
853
1034
|
const componentFinalVars = [];
|
|
854
1035
|
components.forEach((component) => {
|
|
855
1036
|
// eslint-disable-next-line no-use-before-define
|
|
856
|
-
const { code: componentCode, finalVar } = decompileNode(component, indent, nextVar);
|
|
1037
|
+
const { code: componentCode, finalVar } = decompileNode(component, indent, nextVar, verbose);
|
|
857
1038
|
lines.push(componentCode);
|
|
858
1039
|
// Note: metaLeadingBond is now handled in component constructors via metadata
|
|
859
1040
|
// or leadingBond option, so no mutation needed here
|
|
@@ -868,21 +1049,21 @@ function decompileMolecule(molecule, indent, nextVar) {
|
|
|
868
1049
|
return { code: lines.join('\n'), finalVar: finalVarName };
|
|
869
1050
|
}
|
|
870
1051
|
|
|
871
|
-
function decompileNode(node, indent, nextVar) {
|
|
1052
|
+
function decompileNode(node, indent, nextVar, verbose = true) {
|
|
872
1053
|
if (isRingNode(node)) {
|
|
873
|
-
return decompileRing(node, indent, nextVar);
|
|
1054
|
+
return decompileRing(node, indent, nextVar, verbose);
|
|
874
1055
|
}
|
|
875
1056
|
|
|
876
1057
|
if (isLinearNode(node)) {
|
|
877
|
-
return decompileLinear(node, indent, nextVar);
|
|
1058
|
+
return decompileLinear(node, indent, nextVar, verbose);
|
|
878
1059
|
}
|
|
879
1060
|
|
|
880
1061
|
if (isFusedRingNode(node)) {
|
|
881
|
-
return decompileFusedRing(node, indent, nextVar);
|
|
1062
|
+
return decompileFusedRing(node, indent, nextVar, verbose);
|
|
882
1063
|
}
|
|
883
1064
|
|
|
884
1065
|
if (isMoleculeNode(node)) {
|
|
885
|
-
return decompileMolecule(node, indent, nextVar);
|
|
1066
|
+
return decompileMolecule(node, indent, nextVar, verbose);
|
|
886
1067
|
}
|
|
887
1068
|
|
|
888
1069
|
throw new Error(`Unknown node type: ${node.type}`);
|
|
@@ -894,15 +1075,19 @@ function decompileNode(node, indent, nextVar) {
|
|
|
894
1075
|
* @param {Object} options - Options
|
|
895
1076
|
* @param {number} options.indent - Indentation level (default 0)
|
|
896
1077
|
* @param {string} options.varName - Variable name prefix (default 'v')
|
|
1078
|
+
* @param {boolean} options.verbose - Use verbose constructor syntax (default false).
|
|
1079
|
+
* When false, uses Fragment('SMILES') for Ring and simple Linear nodes.
|
|
897
1080
|
* @param {boolean} options.includeMetadata - Include metadata assignments
|
|
898
1081
|
* (default true). Set to false for cleaner output (but code may not work)
|
|
899
1082
|
*/
|
|
900
1083
|
export function decompile(node, options = {}) {
|
|
901
|
-
const {
|
|
1084
|
+
const {
|
|
1085
|
+
indent = 0, varName = 'v', includeMetadata = true, verbose = false,
|
|
1086
|
+
} = options;
|
|
902
1087
|
const indentStr = ' '.repeat(indent);
|
|
903
1088
|
const nextVar = createCounter(varName);
|
|
904
1089
|
|
|
905
|
-
const { code } = decompileNode(node, indentStr, nextVar);
|
|
1090
|
+
const { code } = decompileNode(node, indentStr, nextVar, verbose);
|
|
906
1091
|
|
|
907
1092
|
// Always use export for declarations
|
|
908
1093
|
let result = code.replace(/^(\s*)(const|let) /gm, '$1export $2 ');
|