smiles-js 0.1.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.
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+
22
+ - name: Run tests
23
+ run: npm test
@@ -0,0 +1,50 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+ release:
8
+ types: [created]
9
+ workflow_dispatch:
10
+ inputs:
11
+ version:
12
+ description: 'Version to publish (leave empty to use package.json version)'
13
+ required: false
14
+
15
+ jobs:
16
+ publish:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ id-token: write # Required for npm provenance
21
+
22
+ steps:
23
+ - name: Checkout code
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Setup Node.js
27
+ uses: actions/setup-node@v4
28
+ with:
29
+ node-version: '20'
30
+ registry-url: 'https://registry.npmjs.org'
31
+
32
+ - name: Run tests
33
+ run: npm test
34
+
35
+ - name: Publish to npm
36
+ run: npm publish --provenance --access public
37
+ env:
38
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
39
+
40
+ - name: Create GitHub Release Summary
41
+ run: |
42
+ echo "## 📦 Published to npm" >> $GITHUB_STEP_SUMMARY
43
+ echo "" >> $GITHUB_STEP_SUMMARY
44
+ echo "Package: \`smiles-js\`" >> $GITHUB_STEP_SUMMARY
45
+ echo "Version: \`$(node -p "require('./package.json').version")\`" >> $GITHUB_STEP_SUMMARY
46
+ echo "" >> $GITHUB_STEP_SUMMARY
47
+ echo "### Install" >> $GITHUB_STEP_SUMMARY
48
+ echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
49
+ echo "npm install smiles-js" >> $GITHUB_STEP_SUMMARY
50
+ echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Souradeep Nanda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,353 @@
1
+ # Molecular DSL
2
+
3
+ A JavaScript library for building molecules using composable fragments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install smiles-js
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```js
14
+ import { Fragment, Ring, FusedRings } from 'smiles-js';
15
+
16
+ const methyl = Fragment('C');
17
+ const benzene = Ring('c', 6);
18
+ const toluene = benzene(methyl);
19
+
20
+ console.log(toluene); // c1ccccc1C
21
+ ```
22
+
23
+ ## Core Concepts
24
+
25
+ Molecules are built by composing fragments. There are three composition operations:
26
+
27
+ | Operation | Syntax | Result |
28
+ |-----------|--------|--------|
29
+ | Branch | `a(b)` | `b` attached as branch to `a` |
30
+ | Multiple branches | `a(b)(c)(d)` | `b`, `c`, `d` all branch from `a` |
31
+ | Nested branches | `a(b(c))` | `c` branches from `b`, which branches from `a` |
32
+
33
+ ## API
34
+
35
+ ### Fragment(smiles)
36
+
37
+ Creates a fragment from a SMILES string.
38
+
39
+ ```js
40
+ const methyl = Fragment('C');
41
+ const ethyl = Fragment('CC');
42
+ const hydroxyl = Fragment('O');
43
+ const carbonyl = Fragment('C=O');
44
+ const carboxyl = Fragment('C(=O)O');
45
+ ```
46
+
47
+ Fragments are composable:
48
+
49
+ ```js
50
+ const ethanol = ethyl(hydroxyl); // CC(O)
51
+ const acetone = methyl(Fragment('=O'))(methyl); // CC(=O)C
52
+ ```
53
+
54
+ Nested branching:
55
+
56
+ ```js
57
+ const a = Fragment('C');
58
+ const b = Fragment('CC');
59
+ const c = Fragment('CCC');
60
+
61
+ const molecule = a(b(c)); // C(CC(CCC))
62
+ ```
63
+
64
+ ### Ring(atom, size)
65
+
66
+ Creates a simple ring.
67
+
68
+ ```js
69
+ const benzene = Ring('c', 6); // c1ccccc1
70
+ const cyclohexane = Ring('C', 6); // C1CCCCC1
71
+ const cyclopentane = Ring('C', 5); // C1CCCC1
72
+ const pyridine = Ring('c', 6, { replace: { 0: 'n' } }); // n1ccccc1
73
+ ```
74
+
75
+ **Parameters:**
76
+
77
+ - `atom` — The atom type. Lowercase for aromatic, uppercase for aliphatic.
78
+ - `size` — Number of atoms in the ring (3-8 typical).
79
+ - `options.replace` — Object mapping positions to different atoms.
80
+
81
+ **Substituted rings:**
82
+
83
+ ```js
84
+ const benzene = Ring('c', 6);
85
+ const toluene = benzene(methyl); // c1ccccc1C
86
+ const xylene = benzene(methyl)(methyl); // c1ccccc1(C)C
87
+ ```
88
+
89
+ ### FusedRings(sizes, atom, options?)
90
+
91
+ Creates fused ring systems.
92
+
93
+ ```js
94
+ const naphthalene = FusedRings([6, 6], 'c'); // c1ccc2ccccc2c1
95
+ const anthracene = FusedRings([6, 6, 6], 'c'); // linear fusion
96
+ const indene = FusedRings([6, 5], 'c'); // 6-membered fused to 5-membered
97
+ ```
98
+
99
+ **Parameters:**
100
+
101
+ - `sizes` — Array of ring sizes, fused linearly.
102
+ - `atom` — Default atom type.
103
+ - `options.hetero` — Object mapping positions to heteroatoms.
104
+
105
+ **Examples:**
106
+
107
+ ```js
108
+ // Indole: benzene fused to pyrrole
109
+ const indole = FusedRings([6, 5], 'c', {
110
+ hetero: { 8: '[nH]' }
111
+ });
112
+
113
+ // Quinoline: benzene fused to pyridine
114
+ const quinoline = FusedRings([6, 6], 'c', {
115
+ hetero: { 0: 'n' }
116
+ });
117
+ ```
118
+
119
+ ### Repeat(fragment, count)
120
+
121
+ Creates repeating units for polymers and chains.
122
+
123
+ ```js
124
+ const hexane = Repeat('C', 6); // CCCCCC
125
+ const peg = Repeat('CCO', 4); // CCOCCOCCOCCOCCO
126
+ const polyethylene = Repeat('CC', 100); // CC...CC (200 carbons)
127
+ ```
128
+
129
+ **Parameters:**
130
+
131
+ - `fragment` — SMILES string or Fragment to repeat.
132
+ - `count` — Number of repetitions.
133
+
134
+ ## Composition
135
+
136
+ ### Branching with `()`
137
+
138
+ Calling a fragment with another fragment creates a branch:
139
+
140
+ ```js
141
+ const methyl = Fragment('C');
142
+ const ethyl = Fragment('CC');
143
+
144
+ methyl(ethyl); // C(CC)
145
+ methyl(ethyl)(ethyl); // C(CC)(CC)
146
+ ```
147
+
148
+ The branch attaches to the last atom of the parent fragment.
149
+
150
+ ### Nested Branches
151
+
152
+ Branches can be nested to any depth:
153
+
154
+ ```js
155
+ const C = Fragment('C');
156
+ const CC = Fragment('CC');
157
+ const CCC = Fragment('CCC');
158
+
159
+ C(CC(CCC)); // C(CC(CCC))
160
+ ```
161
+
162
+ This creates:
163
+
164
+ ```
165
+ C ─ C ─ C
166
+ │
167
+ C ─ C ─ C
168
+ ```
169
+
170
+ A more complex example:
171
+
172
+ ```js
173
+ const methyl = Fragment('C');
174
+ const ethyl = Fragment('CC');
175
+ const propyl = Fragment('CCC');
176
+ const butyl = Fragment('CCCC');
177
+
178
+ // Central carbon with 4 different branches
179
+ methyl(ethyl)(propyl(butyl))(methyl); // C(CC)(CCC(CCCC))(C)
180
+ ```
181
+
182
+ ## Common Fragments
183
+
184
+ The library includes common fragments:
185
+
186
+ ```js
187
+ import {
188
+ // Alkyls
189
+ methyl, // C
190
+ ethyl, // CC
191
+ propyl, // CCC
192
+ isopropyl, // C(C)C
193
+ butyl, // CCCC
194
+ tbutyl, // C(C)(C)C
195
+
196
+ // Functional groups
197
+ hydroxyl, // O
198
+ amino, // N
199
+ carboxyl, // C(=O)O
200
+ carbonyl, // C=O
201
+ nitro, // [N+](=O)[O-]
202
+ cyano, // C#N
203
+
204
+ // Halogens
205
+ fluoro, // F
206
+ chloro, // Cl
207
+ bromo, // Br
208
+ iodo, // I
209
+
210
+ // Rings
211
+ benzene, // c1ccccc1
212
+ cyclohexane,// C1CCCCC1
213
+ pyridine, // n1ccccc1
214
+ pyrrole, // c1cc[nH]c1
215
+ furan, // c1ccoc1
216
+ thiophene, // c1ccsc1
217
+
218
+ // Fused rings
219
+ naphthalene, // c1ccc2ccccc2c1
220
+ indole, // c1ccc2[nH]ccc2c1
221
+ quinoline, // n1ccc2ccccc2c1
222
+ } from 'molecular-dsl/common';
223
+ ```
224
+
225
+ ## Properties
226
+
227
+ Every fragment exposes:
228
+
229
+ ```js
230
+ const mol = benzene(methyl);
231
+
232
+ mol.smiles; // "c1ccccc1C"
233
+ mol.atoms; // 7
234
+ mol.rings; // 1
235
+ mol.formula; // "C7H8"
236
+ mol.molecularWeight; // 92.14
237
+
238
+ console.log(mol); // c1ccccc1C
239
+ ```
240
+
241
+ ## Validation
242
+
243
+ Fragments validate on creation:
244
+
245
+ ```js
246
+ Fragment('C(C'); // Error: Unclosed branch
247
+
248
+ Fragment('c1ccc1'); // Error: Invalid ring closure
249
+ ```
250
+
251
+ To check validity without throwing:
252
+
253
+ ```js
254
+ const result = Fragment.validate('C(C');
255
+ // { valid: false, error: 'Unclosed branch' }
256
+
257
+ const result = Fragment.validate('CCO');
258
+ // { valid: true }
259
+ ```
260
+
261
+ ## Advanced SMILES Features
262
+
263
+ For features not covered by the DSL, use raw SMILES in `Fragment()`:
264
+
265
+ **Charges:**
266
+
267
+ ```js
268
+ const ammonium = Fragment('[NH4+]');
269
+ const carboxylate = Fragment('[O-]');
270
+ ```
271
+
272
+ **Explicit hydrogens:**
273
+
274
+ ```js
275
+ const pyrroleN = Fragment('[nH]');
276
+ ```
277
+
278
+ **Stereochemistry:**
279
+
280
+ ```js
281
+ const lAlanine = Fragment('C[C@H](N)C(=O)O');
282
+ const transButene = Fragment('C/C=C/C');
283
+ ```
284
+
285
+ **Isotopes:**
286
+
287
+ ```js
288
+ const deuterium = Fragment('[2H]');
289
+ const carbon13 = Fragment('[13C]');
290
+ ```
291
+
292
+ ## Examples
293
+
294
+ ### Aspirin
295
+
296
+ ```js
297
+ const aspirin = benzene(carboxyl)(Fragment('OC(=O)C'));
298
+ ```
299
+
300
+ ### Caffeine
301
+
302
+ ```js
303
+ const caffeine = Fragment('Cn1cnc2c1c(=O)n(c(=O)n2C)C');
304
+ ```
305
+
306
+ ### Ibuprofen
307
+
308
+ ```js
309
+ const ibuprofen = benzene(
310
+ Fragment('CC(C)C')
311
+ )(
312
+ Fragment('CC(C)C(=O)O')
313
+ );
314
+ ```
315
+
316
+ ### Building a library
317
+
318
+ ```js
319
+ import { Fragment, benzene, methyl, hydroxyl, carboxyl } from 'molecular-dsl';
320
+
321
+ // Define your fragments
322
+ const acetyl = Fragment('C(=O)C');
323
+ const phenyl = benzene;
324
+
325
+ // Compose molecules
326
+ const molecules = {
327
+ toluene: phenyl(methyl),
328
+ phenol: phenyl(hydroxyl),
329
+ benzoicAcid: phenyl(carboxyl),
330
+ acetophenone: phenyl(acetyl),
331
+ };
332
+
333
+ // Export SMILES
334
+ for (const [name, mol] of Object.entries(molecules)) {
335
+ console.log(`${name}: ${mol}`);
336
+ }
337
+ ```
338
+
339
+ Output:
340
+
341
+ ```
342
+ toluene: c1ccccc1C
343
+ phenol: c1ccccc1O
344
+ benzoicAcid: c1ccccc1C(=O)O
345
+ acetophenone: c1ccccc1C(=O)C
346
+ ```
347
+
348
+ ### Polymer
349
+
350
+ ```js
351
+ const peg = Repeat('OCO', 10);
352
+ console.log(peg); // OCOOCOOCOOCOOCOOCOOCOOCOOCOOCO
353
+ ```
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "smiles-js",
3
+ "version": "0.1.0",
4
+ "description": "A JavaScript library for building molecules using composable fragments",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./common": "./src/common.js"
10
+ },
11
+ "scripts": {
12
+ "test": "node --test"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/Ghost---Shadow/smiles-js"
17
+ },
18
+ "keywords": [
19
+ "smiles",
20
+ "chemistry",
21
+ "molecules",
22
+ "dsl"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT"
26
+ }
package/src/common.js ADDED
@@ -0,0 +1,33 @@
1
+ import { Fragment } from './fragment.js';
2
+ import { Ring } from './ring.js';
3
+ import { FusedRings } from './fused-rings.js';
4
+
5
+ export const methyl = Fragment('C');
6
+ export const ethyl = Fragment('CC');
7
+ export const propyl = Fragment('CCC');
8
+ export const isopropyl = Fragment('C(C)C');
9
+ export const butyl = Fragment('CCCC');
10
+ export const tbutyl = Fragment('C(C)(C)C');
11
+
12
+ export const hydroxyl = Fragment('O');
13
+ export const amino = Fragment('N');
14
+ export const carboxyl = Fragment('C(=O)O');
15
+ export const carbonyl = Fragment('C=O');
16
+ export const nitro = Fragment('[N+](=O)[O-]');
17
+ export const cyano = Fragment('C#N');
18
+
19
+ export const fluoro = Fragment('F');
20
+ export const chloro = Fragment('Cl');
21
+ export const bromo = Fragment('Br');
22
+ export const iodo = Fragment('I');
23
+
24
+ export const benzene = Ring('c', 6);
25
+ export const cyclohexane = Ring('C', 6);
26
+ export const pyridine = Ring('c', 6, { replace: { 0: 'n' } });
27
+ export const pyrrole = Ring('c', 5, { replace: { 0: '[nH]' } });
28
+ export const furan = Ring('c', 5, { replace: { 0: 'o' } });
29
+ export const thiophene = Ring('c', 5, { replace: { 0: 's' } });
30
+
31
+ export const naphthalene = FusedRings([6, 6], 'c');
32
+ export const indole = FusedRings([6, 5], 'c', { hetero: { 7: '[nH]' } });
33
+ export const quinoline = FusedRings([6, 6], 'c', { hetero: { 0: 'n' } });
@@ -0,0 +1,49 @@
1
+ import { validateSMILES } from './validator.js';
2
+ import { countAtoms, countRings, calculateFormula, calculateMolecularWeight } from './properties.js';
3
+ import { findUsedRingNumbers, getNextRingNumber } from './utils.js';
4
+
5
+ export function Fragment(smiles) {
6
+ const validation = validateSMILES(smiles);
7
+ if (!validation.valid) {
8
+ throw new Error(validation.error);
9
+ }
10
+
11
+ const createFragment = (currentSmiles) => {
12
+ const fragment = function(...branches) {
13
+ let result = currentSmiles;
14
+
15
+ for (const branch of branches) {
16
+ const branchSmiles = typeof branch === 'function' ? branch.smiles : String(branch);
17
+
18
+ const usedInParent = findUsedRingNumbers(result);
19
+ const usedInBranch = findUsedRingNumbers(branchSmiles);
20
+
21
+ let remappedBranch = branchSmiles;
22
+ for (const ringNum of usedInBranch) {
23
+ if (usedInParent.has(ringNum)) {
24
+ const newNum = getNextRingNumber(result + remappedBranch);
25
+ remappedBranch = remappedBranch.replaceAll(ringNum, newNum.replace('%', ''));
26
+ }
27
+ }
28
+
29
+ result += `(${remappedBranch})`;
30
+ }
31
+
32
+ return createFragment(result);
33
+ };
34
+
35
+ fragment.smiles = currentSmiles;
36
+ fragment.atoms = countAtoms(currentSmiles);
37
+ fragment.rings = countRings(currentSmiles);
38
+ fragment.formula = calculateFormula(currentSmiles);
39
+ fragment.molecularWeight = calculateMolecularWeight(currentSmiles);
40
+ fragment.toString = () => currentSmiles;
41
+ fragment[Symbol.toPrimitive] = () => currentSmiles;
42
+
43
+ return fragment;
44
+ };
45
+
46
+ return createFragment(smiles);
47
+ }
48
+
49
+ Fragment.validate = validateSMILES;
@@ -0,0 +1,38 @@
1
+ import { Fragment } from './fragment.js';
2
+
3
+ export function FusedRings(sizes, atom, options = {}) {
4
+ const { hetero = {} } = options;
5
+
6
+ if (sizes.length !== 2) {
7
+ throw new Error('FusedRings currently only supports 2 rings');
8
+ }
9
+
10
+ let smiles = '';
11
+ let atomPosition = 0;
12
+
13
+ smiles += (hetero[atomPosition] !== undefined ? hetero[atomPosition] : atom) + '1';
14
+ atomPosition++;
15
+
16
+ const firstRingMiddleAtoms = sizes[0] - 4;
17
+ for (let i = 0; i < firstRingMiddleAtoms; i++) {
18
+ smiles += (hetero[atomPosition] !== undefined ? hetero[atomPosition] : atom);
19
+ atomPosition++;
20
+ }
21
+
22
+ smiles += (hetero[atomPosition] !== undefined ? hetero[atomPosition] : atom) + '2';
23
+ atomPosition++;
24
+
25
+ const ringSize = sizes[1];
26
+ const middleAtoms = ringSize - 2;
27
+ for (let i = 0; i < middleAtoms; i++) {
28
+ smiles += (hetero[atomPosition] !== undefined ? hetero[atomPosition] : atom);
29
+ atomPosition++;
30
+ }
31
+
32
+ smiles += (hetero[atomPosition] !== undefined ? hetero[atomPosition] : atom) + '2';
33
+ atomPosition++;
34
+
35
+ smiles += (hetero[atomPosition] !== undefined ? hetero[atomPosition] : atom) + '1';
36
+
37
+ return Fragment(smiles);
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Fragment } from './fragment.js';
2
+ export { Ring } from './ring.js';
3
+ export { FusedRings } from './fused-rings.js';
4
+ export { Repeat } from './repeat.js';
@@ -0,0 +1,219 @@
1
+ const ATOMIC_WEIGHTS = {
2
+ 'H': 1.008, 'C': 12.011, 'N': 14.007, 'O': 15.999, 'F': 18.998,
3
+ 'P': 30.974, 'S': 32.06, 'Cl': 35.45, 'Br': 79.904, 'I': 126.90,
4
+ 'B': 10.81, 'Si': 28.085, 'Se': 78.971, 'Na': 22.990, 'K': 39.098,
5
+ 'Ca': 40.078, 'Fe': 55.845, 'Zn': 65.38, 'Mg': 24.305
6
+ };
7
+
8
+ function parseAtom(smiles, index) {
9
+ if (smiles[index] === '[') {
10
+ const end = smiles.indexOf(']', index);
11
+ if (end === -1) return null;
12
+ const bracketContent = smiles.substring(index + 1, end);
13
+
14
+ let isotope = '';
15
+ let atom = '';
16
+ let i = 0;
17
+
18
+ while (i < bracketContent.length && bracketContent[i] >= '0' && bracketContent[i] <= '9') {
19
+ isotope += bracketContent[i];
20
+ i++;
21
+ }
22
+
23
+ if (i < bracketContent.length && bracketContent[i] >= 'A' && bracketContent[i] <= 'Z') {
24
+ atom = bracketContent[i];
25
+ i++;
26
+ if (i < bracketContent.length && bracketContent[i] >= 'a' && bracketContent[i] <= 'z') {
27
+ atom += bracketContent[i];
28
+ }
29
+ } else if (i < bracketContent.length && bracketContent[i] >= 'a' && bracketContent[i] <= 'z') {
30
+ atom = bracketContent[i];
31
+ }
32
+
33
+ return { atom, length: end - index + 1 };
34
+ }
35
+
36
+ if (smiles[index] >= 'A' && smiles[index] <= 'Z') {
37
+ let atom = smiles[index];
38
+ if (index + 1 < smiles.length && smiles[index + 1] >= 'a' && smiles[index + 1] <= 'z') {
39
+ atom += smiles[index + 1];
40
+ return { atom, length: 2 };
41
+ }
42
+ return { atom, length: 1 };
43
+ }
44
+
45
+ if (smiles[index] >= 'a' && smiles[index] <= 'z') {
46
+ return { atom: smiles[index].toUpperCase(), length: 1 };
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ export function countAtoms(smiles) {
53
+ let count = 0;
54
+ let i = 0;
55
+
56
+ while (i < smiles.length) {
57
+ const char = smiles[i];
58
+
59
+ if (char === '(' || char === ')' || char === '=' || char === '#' ||
60
+ char === '/' || char === '\\' || char === '@' || char === '+' || char === '-') {
61
+ i++;
62
+ continue;
63
+ }
64
+
65
+ if (char >= '0' && char <= '9') {
66
+ i++;
67
+ continue;
68
+ }
69
+
70
+ if (char === '%') {
71
+ i += 3;
72
+ continue;
73
+ }
74
+
75
+ const parsed = parseAtom(smiles, i);
76
+ if (parsed) {
77
+ count++;
78
+ i += parsed.length;
79
+ } else {
80
+ i++;
81
+ }
82
+ }
83
+
84
+ return count;
85
+ }
86
+
87
+ export function countRings(smiles) {
88
+ const ringNumbers = new Set();
89
+ let count = 0;
90
+ let i = 0;
91
+
92
+ while (i < smiles.length) {
93
+ const char = smiles[i];
94
+
95
+ if (char >= '0' && char <= '9') {
96
+ if (ringNumbers.has(char)) {
97
+ count++;
98
+ ringNumbers.delete(char);
99
+ } else {
100
+ ringNumbers.add(char);
101
+ }
102
+ i++;
103
+ } else if (char === '%' && i + 2 < smiles.length) {
104
+ const num = smiles.substring(i + 1, i + 3);
105
+ if (ringNumbers.has(num)) {
106
+ count++;
107
+ ringNumbers.delete(num);
108
+ } else {
109
+ ringNumbers.add(num);
110
+ }
111
+ i += 3;
112
+ } else {
113
+ i++;
114
+ }
115
+ }
116
+
117
+ return count;
118
+ }
119
+
120
+ export function calculateFormula(smiles) {
121
+ const elementCounts = {};
122
+ let i = 0;
123
+
124
+ while (i < smiles.length) {
125
+ const char = smiles[i];
126
+
127
+ if (char === '(' || char === ')' || char === '=' || char === '#' ||
128
+ char === '/' || char === '\\' || char === '@' || char === '+' || char === '-') {
129
+ i++;
130
+ continue;
131
+ }
132
+
133
+ if (char >= '0' && char <= '9') {
134
+ i++;
135
+ continue;
136
+ }
137
+
138
+ if (char === '%') {
139
+ i += 3;
140
+ continue;
141
+ }
142
+
143
+ const parsed = parseAtom(smiles, i);
144
+ if (parsed && parsed.atom) {
145
+ const element = parsed.atom;
146
+ elementCounts[element] = (elementCounts[element] || 0) + 1;
147
+ i += parsed.length;
148
+ } else {
149
+ i++;
150
+ }
151
+ }
152
+
153
+ const implicitHydrogens = calculateImplicitHydrogens(smiles, elementCounts);
154
+ if (implicitHydrogens > 0) {
155
+ elementCounts['H'] = (elementCounts['H'] || 0) + implicitHydrogens;
156
+ }
157
+
158
+ const elements = Object.keys(elementCounts).sort();
159
+
160
+ const hillOrder = (a, b) => {
161
+ if (a === 'C') return -1;
162
+ if (b === 'C') return 1;
163
+ if (a === 'H') return -1;
164
+ if (b === 'H') return 1;
165
+ return a.localeCompare(b);
166
+ };
167
+
168
+ elements.sort(hillOrder);
169
+
170
+ return elements.map(e => {
171
+ const count = elementCounts[e];
172
+ return count === 1 ? e : `${e}${count}`;
173
+ }).join('');
174
+ }
175
+
176
+ function calculateImplicitHydrogens(smiles, elementCounts) {
177
+ let totalHydrogens = 0;
178
+
179
+ for (const [element, count] of Object.entries(elementCounts)) {
180
+ if (element === 'C') {
181
+ totalHydrogens += count * 2 + 2;
182
+ }
183
+ }
184
+
185
+ const bonds = (smiles.match(/=/g) || []).length + (smiles.match(/#/g) || []).length * 2;
186
+ totalHydrogens -= bonds * 2;
187
+
188
+ const explicitHCount = elementCounts['H'] || 0;
189
+ totalHydrogens -= explicitHCount;
190
+
191
+ return Math.max(0, totalHydrogens);
192
+ }
193
+
194
+ export function calculateMolecularWeight(smiles) {
195
+ const formula = calculateFormula(smiles);
196
+ let weight = 0;
197
+ let i = 0;
198
+
199
+ while (i < formula.length) {
200
+ let element = formula[i];
201
+ i++;
202
+
203
+ if (i < formula.length && formula[i] >= 'a' && formula[i] <= 'z') {
204
+ element += formula[i];
205
+ i++;
206
+ }
207
+
208
+ let count = '';
209
+ while (i < formula.length && formula[i] >= '0' && formula[i] <= '9') {
210
+ count += formula[i];
211
+ i++;
212
+ }
213
+
214
+ const atomCount = count ? parseInt(count) : 1;
215
+ weight += (ATOMIC_WEIGHTS[element] || 0) * atomCount;
216
+ }
217
+
218
+ return Math.round(weight * 100) / 100;
219
+ }
package/src/repeat.js ADDED
@@ -0,0 +1,7 @@
1
+ import { Fragment } from './fragment.js';
2
+
3
+ export function Repeat(fragment, count) {
4
+ const smiles = typeof fragment === 'function' ? fragment.smiles : String(fragment);
5
+ const repeated = smiles.repeat(count);
6
+ return Fragment(repeated);
7
+ }
package/src/ring.js ADDED
@@ -0,0 +1,20 @@
1
+ import { Fragment } from './fragment.js';
2
+
3
+ export function Ring(atom, size, options = {}) {
4
+ const { replace = {} } = options;
5
+
6
+ let smiles = '';
7
+ for (let i = 0; i < size; i++) {
8
+ const currentAtom = replace[i] !== undefined ? replace[i] : atom;
9
+
10
+ if (i === 0) {
11
+ smiles += currentAtom + '1';
12
+ } else if (i === size - 1) {
13
+ smiles += currentAtom + '1';
14
+ } else {
15
+ smiles += currentAtom;
16
+ }
17
+ }
18
+
19
+ return Fragment(smiles);
20
+ }
package/src/utils.js ADDED
@@ -0,0 +1,39 @@
1
+ export function findUsedRingNumbers(smiles) {
2
+ const used = new Set();
3
+ let i = 0;
4
+
5
+ while (i < smiles.length) {
6
+ const char = smiles[i];
7
+
8
+ if (char >= '0' && char <= '9') {
9
+ used.add(char);
10
+ i++;
11
+ } else if (char === '%' && i + 2 < smiles.length) {
12
+ const num = smiles.substring(i + 1, i + 3);
13
+ used.add(num);
14
+ i += 3;
15
+ } else {
16
+ i++;
17
+ }
18
+ }
19
+
20
+ return used;
21
+ }
22
+
23
+ export function getNextRingNumber(smiles) {
24
+ const used = findUsedRingNumbers(smiles);
25
+
26
+ for (let i = 1; i <= 9; i++) {
27
+ if (!used.has(String(i))) {
28
+ return String(i);
29
+ }
30
+ }
31
+
32
+ for (let i = 10; i <= 99; i++) {
33
+ if (!used.has(String(i))) {
34
+ return `%${i}`;
35
+ }
36
+ }
37
+
38
+ throw new Error('Too many rings (exhausted ring numbers)');
39
+ }
@@ -0,0 +1,46 @@
1
+ export function validateSMILES(smiles) {
2
+ const errors = [];
3
+ let branchCount = 0;
4
+ let ringNumbers = new Set();
5
+ let openRings = new Set();
6
+
7
+ for (let i = 0; i < smiles.length; i++) {
8
+ const char = smiles[i];
9
+
10
+ if (char === '(') {
11
+ branchCount++;
12
+ } else if (char === ')') {
13
+ branchCount--;
14
+ if (branchCount < 0) {
15
+ return { valid: false, error: 'Unmatched closing branch' };
16
+ }
17
+ } else if (char >= '0' && char <= '9') {
18
+ const num = char;
19
+ if (openRings.has(num)) {
20
+ openRings.delete(num);
21
+ } else {
22
+ openRings.add(num);
23
+ }
24
+ } else if (char === '%') {
25
+ if (i + 2 < smiles.length) {
26
+ const num = smiles.substring(i + 1, i + 3);
27
+ if (openRings.has(num)) {
28
+ openRings.delete(num);
29
+ } else {
30
+ openRings.add(num);
31
+ }
32
+ i += 2;
33
+ }
34
+ }
35
+ }
36
+
37
+ if (branchCount > 0) {
38
+ return { valid: false, error: 'Unclosed branch' };
39
+ }
40
+
41
+ if (openRings.size > 0) {
42
+ return { valid: false, error: 'Invalid ring closure' };
43
+ }
44
+
45
+ return { valid: true };
46
+ }
@@ -0,0 +1,60 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import {
4
+ methyl, ethyl, propyl, isopropyl, butyl, tbutyl,
5
+ hydroxyl, amino, carboxyl, carbonyl, nitro, cyano,
6
+ fluoro, chloro, bromo, iodo,
7
+ benzene, cyclohexane, pyridine, pyrrole, furan, thiophene,
8
+ naphthalene, indole, quinoline
9
+ } from '../src/common.js';
10
+
11
+ test('Common alkyls are defined', () => {
12
+ assert.strictEqual(methyl.smiles, 'C');
13
+ assert.strictEqual(ethyl.smiles, 'CC');
14
+ assert.strictEqual(propyl.smiles, 'CCC');
15
+ assert.strictEqual(isopropyl.smiles, 'C(C)C');
16
+ assert.strictEqual(butyl.smiles, 'CCCC');
17
+ assert.strictEqual(tbutyl.smiles, 'C(C)(C)C');
18
+ });
19
+
20
+ test('Common functional groups are defined', () => {
21
+ assert.strictEqual(hydroxyl.smiles, 'O');
22
+ assert.strictEqual(amino.smiles, 'N');
23
+ assert.strictEqual(carboxyl.smiles, 'C(=O)O');
24
+ assert.strictEqual(carbonyl.smiles, 'C=O');
25
+ assert.strictEqual(nitro.smiles, '[N+](=O)[O-]');
26
+ assert.strictEqual(cyano.smiles, 'C#N');
27
+ });
28
+
29
+ test('Common halogens are defined', () => {
30
+ assert.strictEqual(fluoro.smiles, 'F');
31
+ assert.strictEqual(chloro.smiles, 'Cl');
32
+ assert.strictEqual(bromo.smiles, 'Br');
33
+ assert.strictEqual(iodo.smiles, 'I');
34
+ });
35
+
36
+ test('Common rings are defined', () => {
37
+ assert.strictEqual(benzene.smiles, 'c1ccccc1');
38
+ assert.strictEqual(cyclohexane.smiles, 'C1CCCCC1');
39
+ assert.strictEqual(pyridine.smiles, 'n1ccccc1');
40
+ assert.strictEqual(pyrrole.smiles, '[nH]1cccc1');
41
+ assert.strictEqual(furan.smiles, 'o1cccc1');
42
+ assert.strictEqual(thiophene.smiles, 's1cccc1');
43
+ });
44
+
45
+ test('Common fused rings are defined', () => {
46
+ assert.strictEqual(naphthalene.smiles, 'c1ccc2ccccc2c1');
47
+ assert.strictEqual(indole.smiles, 'c1ccc2ccc[nH]2c1');
48
+ assert.strictEqual(quinoline.smiles, 'n1ccc2ccccc2c1');
49
+ });
50
+
51
+ test('Common fragments can be composed', () => {
52
+ const toluene = benzene(methyl);
53
+ assert.strictEqual(toluene.smiles, 'c1ccccc1(C)');
54
+
55
+ const phenol = benzene(hydroxyl);
56
+ assert.strictEqual(phenol.smiles, 'c1ccccc1(O)');
57
+
58
+ const benzoicAcid = benzene(carboxyl);
59
+ assert.strictEqual(benzoicAcid.smiles, 'c1ccccc1(C(=O)O)');
60
+ });
@@ -0,0 +1,59 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Fragment, Ring } from '../src/index.js';
4
+ import { benzene, methyl, hydroxyl, carboxyl } from '../src/common.js';
5
+
6
+ test('README example: toluene', () => {
7
+ const benzeneRing = Ring('c', 6);
8
+ const methylGroup = Fragment('C');
9
+ const toluene = benzeneRing(methylGroup);
10
+ assert.strictEqual(String(toluene), 'c1ccccc1(C)');
11
+ });
12
+
13
+ test('README example: ethanol', () => {
14
+ const ethyl = Fragment('CC');
15
+ const hydroxylGroup = Fragment('O');
16
+ const ethanol = ethyl(hydroxylGroup);
17
+ assert.strictEqual(ethanol.smiles, 'CC(O)');
18
+ });
19
+
20
+ test('README example: acetone', () => {
21
+ const methylGroup = Fragment('C');
22
+ const acetone = methylGroup(Fragment('=O'))(methylGroup);
23
+ assert.strictEqual(acetone.smiles, 'C(=O)(C)');
24
+ });
25
+
26
+ test('README example: nested branching', () => {
27
+ const a = Fragment('C');
28
+ const b = Fragment('CC');
29
+ const c = Fragment('CCC');
30
+ const molecule = a(b(c));
31
+ assert.strictEqual(molecule.smiles, 'C(CC(CCC))');
32
+ });
33
+
34
+ test('README example: aspirin', () => {
35
+ const aspirin = benzene(carboxyl)(Fragment('OC(=O)C'));
36
+ assert.strictEqual(aspirin.smiles, 'c1ccccc1(C(=O)O)(OC(=O)C)');
37
+ });
38
+
39
+ test('README example: ibuprofen', () => {
40
+ const ibuprofen = benzene(Fragment('CC(C)C'))(Fragment('CC(C)C(=O)O'));
41
+ assert.strictEqual(ibuprofen.smiles, 'c1ccccc1(CC(C)C)(CC(C)C(=O)O)');
42
+ });
43
+
44
+ test('README example: molecule library', () => {
45
+ const acetyl = Fragment('C(=O)C');
46
+ const phenyl = benzene;
47
+
48
+ const molecules = {
49
+ toluene: phenyl(methyl),
50
+ phenol: phenyl(hydroxyl),
51
+ benzoicAcid: phenyl(carboxyl),
52
+ acetophenone: phenyl(acetyl),
53
+ };
54
+
55
+ assert.strictEqual(String(molecules.toluene), 'c1ccccc1(C)');
56
+ assert.strictEqual(String(molecules.phenol), 'c1ccccc1(O)');
57
+ assert.strictEqual(String(molecules.benzoicAcid), 'c1ccccc1(C(=O)O)');
58
+ assert.strictEqual(String(molecules.acetophenone), 'c1ccccc1(C(=O)C)');
59
+ });
@@ -0,0 +1,97 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Fragment } from '../src/fragment.js';
4
+
5
+ test('Fragment creates basic fragments', () => {
6
+ const methyl = Fragment('C');
7
+ assert.strictEqual(methyl.smiles, 'C');
8
+ assert.strictEqual(String(methyl), 'C');
9
+ });
10
+
11
+ test('Fragment validates SMILES on creation', () => {
12
+ assert.throws(() => Fragment('C(C'), { message: 'Unclosed branch' });
13
+ });
14
+
15
+ test('Fragment.validate returns validation result', () => {
16
+ const result1 = Fragment.validate('C(C');
17
+ assert.deepStrictEqual(result1, { valid: false, error: 'Unclosed branch' });
18
+
19
+ const result2 = Fragment.validate('CCO');
20
+ assert.deepStrictEqual(result2, { valid: true });
21
+ });
22
+
23
+ test('Fragment supports branching', () => {
24
+ const methyl = Fragment('C');
25
+ const ethyl = Fragment('CC');
26
+ const result = methyl(ethyl);
27
+ assert.strictEqual(result.smiles, 'C(CC)');
28
+ });
29
+
30
+ test('Fragment supports multiple branches', () => {
31
+ const methyl = Fragment('C');
32
+ const ethyl = Fragment('CC');
33
+ const result = methyl(ethyl)(ethyl);
34
+ assert.strictEqual(result.smiles, 'C(CC)(CC)');
35
+ });
36
+
37
+ test('Fragment supports nested branches', () => {
38
+ const a = Fragment('C');
39
+ const b = Fragment('CC');
40
+ const c = Fragment('CCC');
41
+ const result = a(b(c));
42
+ assert.strictEqual(result.smiles, 'C(CC(CCC))');
43
+ });
44
+
45
+ test('Fragment counts atoms correctly', () => {
46
+ const methyl = Fragment('C');
47
+ assert.strictEqual(methyl.atoms, 1);
48
+
49
+ const ethyl = Fragment('CC');
50
+ assert.strictEqual(ethyl.atoms, 2);
51
+
52
+ const benzene = Fragment('c1ccccc1');
53
+ assert.strictEqual(benzene.atoms, 6);
54
+ });
55
+
56
+ test('Fragment counts rings correctly', () => {
57
+ const methyl = Fragment('C');
58
+ assert.strictEqual(methyl.rings, 0);
59
+
60
+ const benzene = Fragment('c1ccccc1');
61
+ assert.strictEqual(benzene.rings, 1);
62
+
63
+ const naphthalene = Fragment('c1ccc2ccccc2c1');
64
+ assert.strictEqual(naphthalene.rings, 2);
65
+ });
66
+
67
+ test('Fragment calculates formula correctly', () => {
68
+ const methyl = Fragment('C');
69
+ assert.strictEqual(methyl.formula, 'CH4');
70
+
71
+ const ethyl = Fragment('CC');
72
+ assert.strictEqual(ethyl.formula, 'C2H6');
73
+
74
+ const carbonyl = Fragment('C=O');
75
+ assert.strictEqual(carbonyl.formula, 'CH2O');
76
+ });
77
+
78
+ test('Fragment calculates molecular weight correctly', () => {
79
+ const methyl = Fragment('C');
80
+ assert.strictEqual(methyl.molecularWeight, 16.04);
81
+
82
+ const ethyl = Fragment('CC');
83
+ assert.strictEqual(ethyl.molecularWeight, 30.07);
84
+ });
85
+
86
+ test('Fragment works with complex molecules', () => {
87
+ const benzene = Fragment('c1ccccc1');
88
+ const methyl = Fragment('C');
89
+ const toluene = benzene(methyl);
90
+ assert.strictEqual(toluene.smiles, 'c1ccccc1(C)');
91
+ });
92
+
93
+ test('Fragment toString and toPrimitive work correctly', () => {
94
+ const methyl = Fragment('C');
95
+ assert.strictEqual(methyl.toString(), 'C');
96
+ assert.strictEqual(`${methyl}`, 'C');
97
+ });
@@ -0,0 +1,41 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { FusedRings } from '../src/fused-rings.js';
4
+
5
+ test('FusedRings creates naphthalene', () => {
6
+ const naphthalene = FusedRings([6, 6], 'c');
7
+ assert.strictEqual(naphthalene.smiles, 'c1ccc2ccccc2c1');
8
+ });
9
+
10
+ test('FusedRings creates indene', () => {
11
+ const indene = FusedRings([6, 5], 'c');
12
+ assert.strictEqual(indene.smiles, 'c1ccc2cccc2c1');
13
+ });
14
+
15
+ test('FusedRings with hetero creates indole-like structure', () => {
16
+ const indole = FusedRings([6, 5], 'c', { hetero: { 7: '[nH]' } });
17
+ assert.strictEqual(indole.smiles, 'c1ccc2ccc[nH]2c1');
18
+ });
19
+
20
+ test('FusedRings with hetero creates quinoline', () => {
21
+ const quinoline = FusedRings([6, 6], 'c', { hetero: { 0: 'n' } });
22
+ assert.strictEqual(quinoline.smiles, 'n1ccc2ccccc2c1');
23
+ });
24
+
25
+ test('FusedRings throws on single ring', () => {
26
+ assert.throws(() => FusedRings([6], 'c'), { message: 'FusedRings currently only supports 2 rings' });
27
+ });
28
+
29
+ test('FusedRings throws on 3+ rings', () => {
30
+ assert.throws(() => FusedRings([6, 6, 6], 'c'), { message: 'FusedRings currently only supports 2 rings' });
31
+ });
32
+
33
+ test('FusedRings counts atoms correctly', () => {
34
+ const naphthalene = FusedRings([6, 6], 'c');
35
+ assert.strictEqual(naphthalene.atoms, 10);
36
+ });
37
+
38
+ test('FusedRings counts rings correctly', () => {
39
+ const naphthalene = FusedRings([6, 6], 'c');
40
+ assert.strictEqual(naphthalene.rings, 2);
41
+ });
@@ -0,0 +1,30 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Repeat } from '../src/repeat.js';
4
+ import { Fragment } from '../src/fragment.js';
5
+
6
+ test('Repeat creates hexane', () => {
7
+ const hexane = Repeat('C', 6);
8
+ assert.strictEqual(hexane.smiles, 'CCCCCC');
9
+ });
10
+
11
+ test('Repeat creates PEG-like chain', () => {
12
+ const peg = Repeat('CCO', 4);
13
+ assert.strictEqual(peg.smiles, 'CCOCCOCCOCCO');
14
+ });
15
+
16
+ test('Repeat creates polyethylene', () => {
17
+ const polyethylene = Repeat('CC', 100);
18
+ assert.strictEqual(polyethylene.smiles, 'CC'.repeat(100));
19
+ });
20
+
21
+ test('Repeat works with Fragment objects', () => {
22
+ const methyl = Fragment('C');
23
+ const chain = Repeat(methyl, 5);
24
+ assert.strictEqual(chain.smiles, 'CCCCC');
25
+ });
26
+
27
+ test('Repeat counts atoms correctly', () => {
28
+ const hexane = Repeat('C', 6);
29
+ assert.strictEqual(hexane.atoms, 6);
30
+ });
@@ -0,0 +1,54 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Ring } from '../src/ring.js';
4
+ import { Fragment } from '../src/fragment.js';
5
+
6
+ test('Ring composition remaps ring numbers correctly', () => {
7
+ const benzene = Ring('c', 6);
8
+ const biphenyl = benzene(benzene);
9
+ assert.strictEqual(biphenyl.smiles, 'c1ccccc1(c2ccccc2)');
10
+ });
11
+
12
+ test('Multiple ring compositions remap correctly', () => {
13
+ const benzene = Ring('c', 6);
14
+ const triphenyl = benzene(benzene)(benzene);
15
+ assert.strictEqual(triphenyl.smiles, 'c1ccccc1(c2ccccc2)(c3ccccc3)');
16
+ });
17
+
18
+ test('Different sized rings compose correctly', () => {
19
+ const benzene = Ring('c', 6);
20
+ const cyclopentane = Ring('C', 5);
21
+ const result = benzene(cyclopentane);
22
+ assert.strictEqual(result.smiles, 'c1ccccc1(C2CCCC2)');
23
+ });
24
+
25
+ test('Ring with existing branches composes correctly', () => {
26
+ const benzene = Ring('c', 6);
27
+ const methyl = Fragment('C');
28
+ const toluene = benzene(methyl);
29
+ const result = toluene(benzene);
30
+ assert.strictEqual(result.smiles, 'c1ccccc1(C)(c2ccccc2)');
31
+ });
32
+
33
+ test('Nested ring compositions work correctly', () => {
34
+ const benzene = Ring('c', 6);
35
+ const cyclohexane = Ring('C', 6);
36
+ const inner = benzene(cyclohexane);
37
+ const outer = benzene(inner);
38
+ assert.strictEqual(outer.smiles, 'c1ccccc1(c3ccccc3(C2CCCCC2))');
39
+ });
40
+
41
+ test('Ring composition counts atoms correctly', () => {
42
+ const benzene = Ring('c', 6);
43
+ const biphenyl = benzene(benzene);
44
+ assert.strictEqual(biphenyl.atoms, 12);
45
+ });
46
+
47
+ test('Ring composition counts rings correctly', () => {
48
+ const benzene = Ring('c', 6);
49
+ const biphenyl = benzene(benzene);
50
+ assert.strictEqual(biphenyl.rings, 2);
51
+
52
+ const triphenyl = benzene(benzene)(benzene);
53
+ assert.strictEqual(triphenyl.rings, 3);
54
+ });
@@ -0,0 +1,51 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Ring } from '../src/ring.js';
4
+ import { Fragment } from '../src/fragment.js';
5
+
6
+ test('Ring creates benzene', () => {
7
+ const benzene = Ring('c', 6);
8
+ assert.strictEqual(benzene.smiles, 'c1ccccc1');
9
+ });
10
+
11
+ test('Ring creates cyclohexane', () => {
12
+ const cyclohexane = Ring('C', 6);
13
+ assert.strictEqual(cyclohexane.smiles, 'C1CCCCC1');
14
+ });
15
+
16
+ test('Ring creates cyclopentane', () => {
17
+ const cyclopentane = Ring('C', 5);
18
+ assert.strictEqual(cyclopentane.smiles, 'C1CCCC1');
19
+ });
20
+
21
+ test('Ring with replace option creates pyridine', () => {
22
+ const pyridine = Ring('c', 6, { replace: { 0: 'n' } });
23
+ assert.strictEqual(pyridine.smiles, 'n1ccccc1');
24
+ });
25
+
26
+ test('Ring supports substitution', () => {
27
+ const benzene = Ring('c', 6);
28
+ const methyl = Fragment('C');
29
+ const toluene = benzene(methyl);
30
+ assert.strictEqual(toluene.smiles, 'c1ccccc1(C)');
31
+ });
32
+
33
+ test('Ring supports multiple substitutions', () => {
34
+ const benzene = Ring('c', 6);
35
+ const methyl = Fragment('C');
36
+ const xylene = benzene(methyl)(methyl);
37
+ assert.strictEqual(xylene.smiles, 'c1ccccc1(C)(C)');
38
+ });
39
+
40
+ test('Ring counts atoms correctly', () => {
41
+ const benzene = Ring('c', 6);
42
+ assert.strictEqual(benzene.atoms, 6);
43
+
44
+ const cyclopentane = Ring('C', 5);
45
+ assert.strictEqual(cyclopentane.atoms, 5);
46
+ });
47
+
48
+ test('Ring counts rings correctly', () => {
49
+ const benzene = Ring('c', 6);
50
+ assert.strictEqual(benzene.rings, 1);
51
+ });