selfies-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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/package.json +65 -0
  4. package/src/alphabet.js +150 -0
  5. package/src/alphabet.test.js +82 -0
  6. package/src/chemistryValidator.js +236 -0
  7. package/src/cli.js +206 -0
  8. package/src/constraints.js +186 -0
  9. package/src/constraints.test.js +126 -0
  10. package/src/decoder.js +636 -0
  11. package/src/decoder.test.js +560 -0
  12. package/src/dsl/analyzer.js +170 -0
  13. package/src/dsl/analyzer.test.js +139 -0
  14. package/src/dsl/dsl.test.js +146 -0
  15. package/src/dsl/importer.js +238 -0
  16. package/src/dsl/index.js +32 -0
  17. package/src/dsl/lexer.js +264 -0
  18. package/src/dsl/lexer.test.js +115 -0
  19. package/src/dsl/parser.js +201 -0
  20. package/src/dsl/parser.test.js +148 -0
  21. package/src/dsl/resolver.js +136 -0
  22. package/src/dsl/resolver.test.js +99 -0
  23. package/src/dsl/symbolTable.js +56 -0
  24. package/src/dsl/symbolTable.test.js +68 -0
  25. package/src/dsl/valenceValidator.js +147 -0
  26. package/src/encoder.js +467 -0
  27. package/src/encoder.test.js +61 -0
  28. package/src/errors.js +79 -0
  29. package/src/errors.test.js +91 -0
  30. package/src/grammar_rules.js +146 -0
  31. package/src/index.js +70 -0
  32. package/src/parser.js +96 -0
  33. package/src/parser.test.js +96 -0
  34. package/src/properties/atoms.js +69 -0
  35. package/src/properties/atoms.test.js +116 -0
  36. package/src/properties/formula.js +111 -0
  37. package/src/properties/formula.test.js +95 -0
  38. package/src/properties/molecularWeight.js +80 -0
  39. package/src/properties/molecularWeight.test.js +84 -0
  40. package/src/properties/properties.test.js +77 -0
  41. package/src/renderers/README.md +127 -0
  42. package/src/renderers/svg.js +113 -0
  43. package/src/renderers/svg.test.js +42 -0
  44. package/src/syntax.js +641 -0
  45. package/src/syntax.test.js +363 -0
  46. package/src/tokenizer.js +99 -0
  47. package/src/tokenizer.test.js +55 -0
  48. package/src/validator.js +70 -0
  49. package/src/validator.test.js +44 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Tests for molecular formula generation
3
+ *
4
+ * Note: Also see properties.test.js for more tests
5
+ */
6
+
7
+ import { describe, test, expect } from 'bun:test'
8
+ import { getFormula } from './formula.js'
9
+
10
+ describe('getFormula', () => {
11
+ // TODO: Simple molecules
12
+ test('generates methane formula', () => {
13
+ // TODO: expect(getFormula('[C]')).toBe('CH4')
14
+ })
15
+
16
+ test('generates ethane formula', () => {
17
+ // TODO: expect(getFormula('[C][C]')).toBe('C2H6')
18
+ })
19
+
20
+ test('generates ethanol formula', () => {
21
+ // TODO: expect(getFormula('[C][C][O]')).toBe('C2H6O')
22
+ })
23
+
24
+ test('generates water formula', () => {
25
+ // TODO: expect(getFormula('[O]')).toBe('H2O')
26
+ })
27
+
28
+ // TODO: Hill notation rules
29
+ test('puts carbon first', () => {
30
+ // TODO: const formula = getFormula('[N][C][C]')
31
+ // TODO: expect(formula[0]).toBe('C')
32
+ })
33
+
34
+ test('puts hydrogen second', () => {
35
+ // TODO: const formula = getFormula('[C][C][O]')
36
+ // TODO: expect(formula).toMatch(/^C\d+H/)
37
+ })
38
+
39
+ test('orders other elements alphabetically', () => {
40
+ // TODO: const formula = getFormula('[C][S][N][O]')
41
+ // TODO: After C and H, should be N, O, S (alphabetical)
42
+ })
43
+
44
+ test('omits count of 1', () => {
45
+ // TODO: expect(getFormula('[C][O][O]')).toBe('CH2O2')
46
+ // TODO: Not CH2O2 but C, not C1
47
+ })
48
+
49
+ test('handles no carbon', () => {
50
+ // TODO: const formula = getFormula('[N][O]')
51
+ // TODO: Should still have H first if present, then alphabetical
52
+ })
53
+
54
+ // TODO: Different bond types
55
+ test('accounts for double bonds', () => {
56
+ // TODO: expect(getFormula('[C][=C]')).toBe('C2H4')
57
+ // TODO: Ethene, not ethane
58
+ })
59
+
60
+ test('accounts for triple bonds', () => {
61
+ // TODO: expect(getFormula('[C][#C]')).toBe('C2H2')
62
+ // TODO: Acetylene
63
+ })
64
+
65
+ // TODO: Complex structures
66
+ test('handles branching', () => {
67
+ // TODO: const formula = getFormula('[C][C][Branch1][C][C][C]')
68
+ // TODO: expect(formula).toBe('C4H10') // Isobutane
69
+ })
70
+
71
+ test('handles rings', () => {
72
+ // TODO: const formula = getFormula('[C][=C][C][=C][C][=C][Ring1][=Branch1]')
73
+ // TODO: expect(formula).toBe('C6H6') // Benzene
74
+ })
75
+
76
+ // TODO: Edge cases
77
+ test('handles single atom', () => {
78
+ // TODO: expect(getFormula('[C]')).toBe('CH4')
79
+ })
80
+
81
+ test('handles no hydrogen', () => {
82
+ // TODO: const formula = getFormula('[C][Cl][Cl][Cl][Cl]')
83
+ // TODO: CCl4 - no hydrogen atoms
84
+ // TODO: expect(formula).toBe('CCl4')
85
+ })
86
+
87
+ // TODO: Error cases
88
+ test('throws on invalid SELFIES', () => {
89
+ // TODO: expect(() => getFormula('[Xyz]')).toThrow()
90
+ })
91
+
92
+ test('throws on malformed SELFIES', () => {
93
+ // TODO: expect(() => getFormula('[C][C')).toThrow()
94
+ })
95
+ })
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Molecular Weight - Calculates molecular weight from SELFIES
3
+ *
4
+ * Computes the molecular weight by parsing the SELFIES string,
5
+ * determining the molecular formula, and summing atomic masses.
6
+ */
7
+
8
+ import { tokenize } from '../tokenizer.js'
9
+ import { parse } from '../parser.js'
10
+ import { getAtomicMass } from './atoms.js'
11
+
12
+ /**
13
+ * Calculates molecular weight of a SELFIES molecule
14
+ * @param {string} selfies - SELFIES string
15
+ * @returns {number} Molecular weight in g/mol
16
+ *
17
+ * Example:
18
+ * getMolecularWeight('[C][C][O]') // => 46.068 (ethanol: C2H6O)
19
+ * getMolecularWeight('[C]') // => 16.043 (methane: CH4)
20
+ */
21
+ export function getMolecularWeight(selfies) {
22
+ const tokens = tokenize(selfies)
23
+ const ir = parse(tokens)
24
+
25
+ const counts = countAtoms(ir)
26
+
27
+ let totalWeight = 0
28
+ for (const [element, count] of Object.entries(counts)) {
29
+ totalWeight += getAtomicMass(element) * count
30
+ }
31
+
32
+ return totalWeight
33
+ }
34
+
35
+ /**
36
+ * Counts atoms in molecule IR (including implicit hydrogens)
37
+ * @param {Object} ir - Molecule internal representation
38
+ * @returns {Object} Map of element to count
39
+ *
40
+ * Example return:
41
+ * { 'C': 2, 'H': 6, 'O': 1 } // for ethanol
42
+ */
43
+ function countAtoms(ir) {
44
+ const counts = {}
45
+
46
+ // Count explicit atoms and calculate used valence
47
+ for (const atom of ir.atoms) {
48
+ counts[atom.element] = (counts[atom.element] || 0) + 1
49
+ }
50
+
51
+ // Calculate used valence from bonds
52
+ const usedValence = new Array(ir.atoms.length).fill(0)
53
+ for (const bond of ir.bonds) {
54
+ usedValence[bond.from] += bond.order
55
+ usedValence[bond.to] += bond.order
56
+ }
57
+
58
+ // Add implicit hydrogens
59
+ let totalH = 0
60
+ for (let i = 0; i < ir.atoms.length; i++) {
61
+ const atom = ir.atoms[i]
62
+ const implicitH = Math.max(0, atom.valence - usedValence[i])
63
+ totalH += implicitH
64
+ }
65
+
66
+ if (totalH > 0) {
67
+ counts['H'] = totalH
68
+ }
69
+
70
+ return counts
71
+ }
72
+
73
+ /**
74
+ * Calculates number of implicit hydrogens for an atom
75
+ * @param {Object} atom - Atom from IR
76
+ * @returns {number} Number of implicit hydrogens
77
+ */
78
+ function getImplicitHydrogens(atom) {
79
+ return atom.valence - atom.usedValence
80
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Tests for molecular weight calculation
3
+ *
4
+ * Note: Also see properties.test.js for more tests
5
+ */
6
+
7
+ import { describe, test, expect } from 'bun:test'
8
+ import { getMolecularWeight } from './molecularWeight.js'
9
+
10
+ describe('getMolecularWeight', () => {
11
+ // TODO: Simple molecules
12
+ test('calculates methane (CH4)', () => {
13
+ // TODO: const mw = getMolecularWeight('[C]')
14
+ // TODO: expect(mw).toBeCloseTo(16.043, 2)
15
+ })
16
+
17
+ test('calculates ethane (C2H6)', () => {
18
+ // TODO: const mw = getMolecularWeight('[C][C]')
19
+ // TODO: expect(mw).toBeCloseTo(30.070, 2)
20
+ })
21
+
22
+ test('calculates ethanol (C2H6O)', () => {
23
+ // TODO: const mw = getMolecularWeight('[C][C][O]')
24
+ // TODO: expect(mw).toBeCloseTo(46.068, 2)
25
+ })
26
+
27
+ // TODO: Different elements
28
+ test('includes nitrogen', () => {
29
+ // TODO: const mw = getMolecularWeight('[N][C][C]')
30
+ // TODO: Should include N mass (14.007) + 2 C + H atoms
31
+ })
32
+
33
+ test('includes oxygen', () => {
34
+ // TODO: const mw = getMolecularWeight('[C][=O]')
35
+ // TODO: Should calculate formaldehyde (CH2O)
36
+ })
37
+
38
+ test('includes sulfur', () => {
39
+ // TODO: const mw = getMolecularWeight('[C][S][C]')
40
+ // TODO: Should include S mass (32.06)
41
+ })
42
+
43
+ test('includes halogens', () => {
44
+ // TODO: const mw = getMolecularWeight('[C][Cl]')
45
+ // TODO: Should calculate chloromethane
46
+ })
47
+
48
+ // TODO: Implicit hydrogens
49
+ test('accounts for implicit hydrogens', () => {
50
+ // TODO: const mw = getMolecularWeight('[C]')
51
+ // TODO: C has 4 valence, 0 used → 4 H atoms
52
+ // TODO: MW = 12.011 + 4*1.008 = 16.043
53
+ })
54
+
55
+ test('reduces H for double bonds', () => {
56
+ // TODO: const mw = getMolecularWeight('[C][=C]')
57
+ // TODO: Ethene: C2H4, not C2H6
58
+ })
59
+
60
+ test('reduces H for triple bonds', () => {
61
+ // TODO: const mw = getMolecularWeight('[C][#C]')
62
+ // TODO: Acetylene: C2H2
63
+ })
64
+
65
+ // TODO: Complex structures
66
+ test('handles branching', () => {
67
+ // TODO: const mw = getMolecularWeight('[C][C][Branch1][C][C][C]')
68
+ // TODO: Isobutane: C4H10
69
+ })
70
+
71
+ test('handles rings', () => {
72
+ // TODO: const mw = getMolecularWeight('[C][=C][C][=C][C][=C][Ring1][=Branch1]')
73
+ // TODO: Benzene: C6H6 = 78.114
74
+ })
75
+
76
+ // TODO: Error cases
77
+ test('throws on invalid SELFIES', () => {
78
+ // TODO: expect(() => getMolecularWeight('[Xyz]')).toThrow()
79
+ })
80
+
81
+ test('throws on malformed SELFIES', () => {
82
+ // TODO: expect(() => getMolecularWeight('[C][C')).toThrow()
83
+ })
84
+ })
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for molecular property calculations
3
+ */
4
+
5
+ import { describe, test, expect } from 'bun:test'
6
+ import { getMolecularWeight } from './molecularWeight.js'
7
+ import { getFormula } from './formula.js'
8
+ import { getAtomicMass, getValence } from './atoms.js'
9
+
10
+ describe('getMolecularWeight', () => {
11
+ test('calculates methane molecular weight', () => {
12
+ expect(getMolecularWeight('[C]')).toBeCloseTo(16.043, 1)
13
+ })
14
+
15
+ test('calculates ethanol molecular weight', () => {
16
+ expect(getMolecularWeight('[C][C][O]')).toBeCloseTo(46.068, 1)
17
+ })
18
+
19
+ test('calculates benzene molecular weight', () => {
20
+ // TODO: expect(getMolecularWeight('[C][=C][C][=C][C][=C][Ring1][=Branch1]')).toBeCloseTo(78.114, 2)
21
+ })
22
+
23
+ // TODO: Different elements
24
+ test('calculates with nitrogen', () => {
25
+ // TODO: expect(getMolecularWeight('[N][C][C]')).toBeCloseTo(43.088, 2)
26
+ })
27
+
28
+ test('calculates with chlorine', () => {
29
+ // TODO: expect(getMolecularWeight('[Cl][C][C][Cl]')).toBeCloseTo(98.959, 2)
30
+ })
31
+ })
32
+
33
+ describe('getFormula', () => {
34
+ test('generates methane formula', () => {
35
+ expect(getFormula('[C]')).toBe('CH4')
36
+ })
37
+
38
+ test('generates ethanol formula', () => {
39
+ expect(getFormula('[C][C][O]')).toBe('C2H6O')
40
+ })
41
+
42
+ test('omits count of 1', () => {
43
+ expect(getFormula('[C][O][O]')).toBe('CH4O2')
44
+ })
45
+ })
46
+
47
+ describe('getAtomicMass', () => {
48
+ test('returns carbon mass', () => {
49
+ expect(getAtomicMass('C')).toBeCloseTo(12.011, 2)
50
+ })
51
+
52
+ test('returns oxygen mass', () => {
53
+ expect(getAtomicMass('O')).toBeCloseTo(15.999, 2)
54
+ })
55
+
56
+ test('throws on unsupported element', () => {
57
+ expect(() => getAtomicMass('Xx')).toThrow()
58
+ })
59
+ })
60
+
61
+ describe('getValence', () => {
62
+ test('returns carbon valence', () => {
63
+ expect(getValence('C')).toBe(4)
64
+ })
65
+
66
+ test('returns nitrogen valence', () => {
67
+ expect(getValence('N')).toBe(3)
68
+ })
69
+
70
+ test('returns oxygen valence', () => {
71
+ expect(getValence('O')).toBe(2)
72
+ })
73
+
74
+ test('throws on unsupported element', () => {
75
+ expect(() => getValence('Xx')).toThrow()
76
+ })
77
+ })
@@ -0,0 +1,127 @@
1
+ # SELFIES Renderers
2
+
3
+ This directory contains rendering modules for visualizing molecular structures from SELFIES AST (Abstract Syntax Tree) representations.
4
+
5
+ ## SVG Renderer
6
+
7
+ The SVG renderer converts molecular AST structures into Scalable Vector Graphics (SVG) format for visualization.
8
+
9
+ ### Usage
10
+
11
+ ```javascript
12
+ import { decodeToAST, renderToSVG } from 'selfies-js'
13
+
14
+ // Decode SELFIES to AST
15
+ const ast = decodeToAST('[C][C][O]')
16
+
17
+ // Render to SVG
18
+ const svg = renderToSVG(ast)
19
+
20
+ // Save or use the SVG string
21
+ console.log(svg)
22
+ ```
23
+
24
+ ### Options
25
+
26
+ The `renderToSVG` function accepts an optional options object:
27
+
28
+ ```javascript
29
+ const svg = renderToSVG(ast, {
30
+ width: 400, // Canvas width (default: 400)
31
+ height: 300, // Canvas height (default: 300)
32
+ bondLength: 40, // Length of bonds in pixels (default: 40)
33
+ atomRadius: 15, // Radius of atom circles (default: 15)
34
+ fontSize: 14, // Font size for atom labels (default: 14)
35
+ strokeWidth: 2, // Bond line thickness (default: 2)
36
+ bondColor: '#333', // Color of bonds (default: '#333')
37
+ atomColor: '#000', // Color of atom text (default: '#000')
38
+ backgroundColor: 'transparent', // Background color (default: 'transparent')
39
+ padding: 40 // Padding around the molecule (default: 40)
40
+ })
41
+ ```
42
+
43
+ ### Features
44
+
45
+ - **Automatic Layout**: Molecules are automatically positioned using a simple layout algorithm
46
+ - **Bond Visualization**: Single, double, and triple bonds are rendered with appropriate styling
47
+ - **Element Colors**: Atoms are colored according to chemistry conventions:
48
+ - Carbon (C): Gray
49
+ - Nitrogen (N): Blue
50
+ - Oxygen (O): Red
51
+ - Sulfur (S): Yellow
52
+ - Phosphorus (P): Orange
53
+ - Halogens (F, Cl, Br, I): Various greens and purples
54
+ - **Ring Structures**: Properly handles ring closures
55
+ - **Stereo Chemistry**: Displays stereochemistry notation (e.g., C@, C@@)
56
+ - **Branching**: Correctly visualizes branched molecules
57
+
58
+ ### Examples
59
+
60
+ ```javascript
61
+ // Simple molecule
62
+ const methane = decodeToAST('[C]')
63
+ const svg1 = renderToSVG(methane)
64
+
65
+ // Molecule with double bond
66
+ const ethene = decodeToAST('[C][=C]')
67
+ const svg2 = renderToSVG(ethene)
68
+
69
+ // Branched molecule
70
+ const isobutane = decodeToAST('[C][C][Branch1][C][C][C]')
71
+ const svg3 = renderToSVG(isobutane)
72
+
73
+ // Ring structure
74
+ const cyclopropane = decodeToAST('[C][C][C][Ring1][C]')
75
+ const svg4 = renderToSVG(cyclopropane)
76
+
77
+ // Complex aromatic ring
78
+ const benzene = decodeToAST('[C][=C][C][=C][C][=C][Ring1][=Branch1]')
79
+ const svg5 = renderToSVG(benzene)
80
+ ```
81
+
82
+ ### Saving SVG Files
83
+
84
+ To save SVG output to files (Node.js):
85
+
86
+ ```javascript
87
+ import { writeFileSync } from 'fs'
88
+ import { decodeToAST, renderToSVG } from 'selfies-js'
89
+
90
+ const ast = decodeToAST('[C][C][O]')
91
+ const svg = renderToSVG(ast)
92
+
93
+ writeFileSync('molecule.svg', svg)
94
+ ```
95
+
96
+ ### Browser Usage
97
+
98
+ In the browser, you can insert the SVG directly into the DOM:
99
+
100
+ ```javascript
101
+ import { decodeToAST, renderToSVG } from 'selfies-js'
102
+
103
+ const ast = decodeToAST('[C][C][O]')
104
+ const svg = renderToSVG(ast)
105
+
106
+ // Insert into page
107
+ document.getElementById('molecule-container').innerHTML = svg
108
+ ```
109
+
110
+ ## Future Renderers
111
+
112
+ This directory is designed to accommodate additional renderers in the future:
113
+
114
+ - **Canvas Renderer**: For rendering to HTML5 Canvas
115
+ - **3D Renderer**: For three-dimensional molecular visualization
116
+ - **ASCII Renderer**: For text-based terminal visualization
117
+ - **WebGL Renderer**: For high-performance 3D rendering
118
+
119
+ ## Architecture
120
+
121
+ All renderers follow a common pattern:
122
+
123
+ 1. Accept an AST object with `atoms`, `bonds`, and `rings` arrays
124
+ 2. Accept an optional configuration object
125
+ 3. Return a string or data structure representing the visualization
126
+
127
+ This allows for consistent usage across different rendering backends.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * SVG Renderer using RDKit.js
3
+ *
4
+ * Uses RDKit's MinimalLib to generate proper 2D coordinates and render molecules
5
+ */
6
+
7
+ import initRDKitModule from '@rdkit/rdkit'
8
+ import { decode } from '../decoder.js'
9
+
10
+ let RDKitModule = null
11
+
12
+ /**
13
+ * Initialize RDKit module (async, only done once)
14
+ */
15
+ async function initRDKit() {
16
+ if (!RDKitModule) {
17
+ RDKitModule = await initRDKitModule()
18
+ }
19
+ return RDKitModule
20
+ }
21
+
22
+ /**
23
+ * Default rendering options
24
+ */
25
+ const DEFAULT_OPTIONS = {
26
+ width: 300,
27
+ height: 300,
28
+ backgroundColor: 'transparent'
29
+ }
30
+
31
+ /**
32
+ * Renders a molecular structure to SVG using RDKit
33
+ * @param {Object} ast - AST object (not used, we use SMILES instead)
34
+ * @param {Object} options - Rendering options (optional)
35
+ * @returns {Promise<string>} SVG string representation
36
+ */
37
+ export async function renderToSVG(ast, options = {}) {
38
+ const opts = { ...DEFAULT_OPTIONS, ...options }
39
+
40
+ // We need SMILES for RDKit, so if we have an AST, we need to convert back
41
+ // For now, we'll need the original SELFIES or generate SMILES from AST
42
+ throw new Error('renderToSVG with AST not yet implemented - use renderSelfies instead')
43
+ }
44
+
45
+ /**
46
+ * Renders a SELFIES string to SVG using RDKit
47
+ * @param {string} selfies - SELFIES string
48
+ * @param {Object} options - Rendering options (optional)
49
+ * @returns {Promise<string>} SVG string representation
50
+ */
51
+ export async function renderSelfies(selfies, options = {}) {
52
+ const opts = { ...DEFAULT_OPTIONS, ...options }
53
+
54
+ // Initialize RDKit
55
+ const RDKit = await initRDKit()
56
+
57
+ // Convert SELFIES to SMILES
58
+ const smiles = decode(selfies)
59
+
60
+ // Create molecule from SMILES
61
+ const mol = RDKit.get_mol(smiles)
62
+
63
+ if (!mol || !mol.is_valid()) {
64
+ throw new Error(`Invalid molecule from SMILES: ${smiles}`)
65
+ }
66
+
67
+ // Generate SVG with RDKit
68
+ const svg = mol.get_svg_with_highlights(JSON.stringify({
69
+ width: opts.width,
70
+ height: opts.height,
71
+ addStereoAnnotation: true,
72
+ kekulize: false
73
+ }))
74
+
75
+ // Clean up
76
+ mol.delete()
77
+
78
+ return svg
79
+ }
80
+
81
+ /**
82
+ * Synchronous version that returns SVG directly
83
+ * Note: This will throw if RDKit hasn't been initialized
84
+ * @param {string} selfies - SELFIES string
85
+ * @param {Object} options - Rendering options
86
+ * @returns {string} SVG string
87
+ */
88
+ export function renderSelfiesSync(selfies, options = {}) {
89
+ if (!RDKitModule) {
90
+ throw new Error('RDKit not initialized. Call initRDKit() first or use renderSelfies()')
91
+ }
92
+
93
+ const opts = { ...DEFAULT_OPTIONS, ...options }
94
+ const smiles = decode(selfies)
95
+ const mol = RDKitModule.get_mol(smiles)
96
+
97
+ if (!mol || !mol.is_valid()) {
98
+ throw new Error(`Invalid molecule from SMILES: ${smiles}`)
99
+ }
100
+
101
+ const svg = mol.get_svg_with_highlights(JSON.stringify({
102
+ width: opts.width,
103
+ height: opts.height,
104
+ addStereoAnnotation: true,
105
+ kekulize: false
106
+ }))
107
+
108
+ mol.delete()
109
+ return svg
110
+ }
111
+
112
+ // Export init function
113
+ export { initRDKit }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tests for SVG renderer (using RDKit)
3
+ */
4
+
5
+ import { describe, test, expect, beforeAll } from 'bun:test'
6
+ import { renderSelfies, initRDKit } from './svg.js'
7
+
8
+ describe('SVG Renderer (RDKit)', () => {
9
+ beforeAll(async () => {
10
+ // Initialize RDKit once before all tests
11
+ await initRDKit()
12
+ })
13
+
14
+ test('renders methane', async () => {
15
+ const svg = await renderSelfies('[C]')
16
+ expect(svg).toContain('<svg')
17
+ expect(svg).toContain('</svg>')
18
+ })
19
+
20
+ test('renders ethane', async () => {
21
+ const svg = await renderSelfies('[C][C]')
22
+ expect(svg).toContain('<svg')
23
+ expect(svg).toContain('path') // RDKit uses paths for bonds
24
+ })
25
+
26
+ test('renders benzene', async () => {
27
+ const svg = await renderSelfies('[C][=C][C][=C][C][=C][Ring1][=Branch1]')
28
+ expect(svg).toContain('<svg')
29
+ expect(svg).toContain('path')
30
+ })
31
+
32
+ test('renders toluene', async () => {
33
+ const svg = await renderSelfies('[C][C][=C][C][=C][C][=C][Ring1][=N]')
34
+ expect(svg).toContain('<svg')
35
+ expect(svg).toContain('path')
36
+ })
37
+
38
+ test('accepts custom width and height', async () => {
39
+ const svg = await renderSelfies('[C]', { width: 400, height: 400 })
40
+ expect(svg).toContain('400')
41
+ })
42
+ })