Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/import-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ export function buildYaml(props: Record<string, string>): string {
const lines: string[] = [];
for (const [key, value] of Object.entries(props)) {
if (value.includes('\n')) {
// When the value starts with a newline (e.g. a MOL-block with a blank title
// line), the YAML parser cannot auto-detect indentation from the first line.
// Use an explicit indentation indicator (2) and strip chomping (-) so the
// leading blank line is preserved faithfully.
const header = value.startsWith('\n') ? '|2-' : '|';
// When the first line of the value is blank or whitespace-only (e.g. a
// MOL-block whose title line is empty or a single space), the YAML parser
// cannot reliably auto-detect indentation — it skips leading blank lines and
// picks up the indent from the first "real" line, which already has leading
// spaces, causing it to strip one extra space from every content line on
// re-read. Use an explicit indentation indicator (2) with strip chomping (-).
const firstLine = value.split('\n')[0];
const header = firstLine.trim() === '' ? '|2-' : '|';
lines.push(`${key}: ${header}`);
for (const line of value.split('\n')) {
lines.push(` ${line}`);
Expand Down
116 changes: 116 additions & 0 deletions tests/data/no_first_name.sdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@

-OEChem-04032607062D

24 25 0 0 0 0 0 0 0999 V2000
3.7321 2.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
2.0000 -1.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 -1.0000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
5.5443 0.8047 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
2.8660 0.5000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
5.5443 -0.8047 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
4.5981 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
4.5981 -0.5000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 1.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
2.8660 -0.5000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
6.1279 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 -2.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
5.8550 1.7553 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
2.0000 1.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
6.7479 -0.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
3.1121 -2.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 -2.6200 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
4.3521 -2.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
6.4443 1.5626 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
6.0476 2.3446 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
5.2656 1.9479 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
2.3100 1.5369 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1.4631 1.3100 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1.6900 0.4631 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1 9 2 0 0 0 0
2 10 2 0 0 0 0
3 8 1 0 0 0 0
3 10 1 0 0 0 0
3 12 1 0 0 0 0
4 7 1 0 0 0 0
4 11 1 0 0 0 0
4 13 1 0 0 0 0
5 9 1 0 0 0 0
5 10 1 0 0 0 0
5 14 1 0 0 0 0
6 8 1 0 0 0 0
6 11 2 0 0 0 0
7 8 2 0 0 0 0
7 9 1 0 0 0 0
11 15 1 0 0 0 0
12 16 1 0 0 0 0
12 17 1 0 0 0 0
12 18 1 0 0 0 0
13 19 1 0 0 0 0
13 20 1 0 0 0 0
13 21 1 0 0 0 0
14 22 1 0 0 0 0
14 23 1 0 0 0 0
14 24 1 0 0 0 0
M END
> <PUBCHEM_COMPOUND_CID>
2519

$$$$
caffeine
-OEChem-04032607062D

24 25 0 0 0 0 0 0 0999 V2000
3.7321 2.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
2.0000 -1.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 -1.0000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
5.5443 0.8047 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
2.8660 0.5000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
5.5443 -0.8047 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
4.5981 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
4.5981 -0.5000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 1.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
2.8660 -0.5000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
6.1279 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 -2.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
5.8550 1.7553 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
2.0000 1.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
6.7479 -0.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
3.1121 -2.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
3.7321 -2.6200 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
4.3521 -2.0000 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
6.4443 1.5626 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
6.0476 2.3446 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
5.2656 1.9479 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
2.3100 1.5369 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1.4631 1.3100 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1.6900 0.4631 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1 9 2 0 0 0 0
2 10 2 0 0 0 0
3 8 1 0 0 0 0
3 10 1 0 0 0 0
3 12 1 0 0 0 0
4 7 1 0 0 0 0
4 11 1 0 0 0 0
4 13 1 0 0 0 0
5 9 1 0 0 0 0
5 10 1 0 0 0 0
5 14 1 0 0 0 0
6 8 1 0 0 0 0
6 11 2 0 0 0 0
7 8 2 0 0 0 0
7 9 1 0 0 0 0
11 15 1 0 0 0 0
12 16 1 0 0 0 0
12 17 1 0 0 0 0
12 18 1 0 0 0 0
13 19 1 0 0 0 0
13 20 1 0 0 0 0
13 21 1 0 0 0 0
14 22 1 0 0 0 0
14 23 1 0 0 0 0
14 24 1 0 0 0 0
M END
> <PUBCHEM_COMPOUND_CID>
2519

$$$$
36 changes: 36 additions & 0 deletions tests/data/no_name_spaces.sdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

-INDIGO-04142622472D

6 6 0 0 0 0 0 0 0 0999 V2000
10.1348 -5.5001 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.8652 -5.4996 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.0016 -5.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.8652 -6.5005 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
10.1348 -6.5050 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.0038 -7.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
3 1 2 0 0 0 0
1 5 1 0 0 0 0
5 6 2 0 0 0 0
6 4 1 0 0 0 0
4 2 2 0 0 0 0
2 3 1 0 0 0 0
M END
$$$$

-INDIGO-04

6 6 0 0 0 0 0 0 0 0999 V2000
10.1348 -5.5001 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.8652 -5.4996 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.0016 -5.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.8652 -6.5005 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
10.1348 -6.5050 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11.0038 -7.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
3 1 2 0 0 0 0
1 5 1 0 0 0 0
5 6 2 0 0 0 0
6 4 1 0 0 0 0
4 2 2 0 0 0 0
2 3 1 0 0 0 0
M END
$$$$
23 changes: 23 additions & 0 deletions tests/import-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { buildYaml, uniquePath } from '../src/import-utils';
const HEADER_LINE = ' -OEChem-04032607062D';
const NAMED_MOLBLOCK = `with_name\n${HEADER_LINE}\n\nM END`;
const BLANK_TITLE_MOLBLOCK = `\n${HEADER_LINE}\n\nM END`;
const SPACE_TITLE_MOLBLOCK = ` \n${HEADER_LINE}\n\nM END`;
const MULTI_SPACE_TITLE_MOLBLOCK = ` \n${HEADER_LINE}\n\nM END`;

describe('buildYaml – block scalar headers', () => {
it('named molblock uses plain | header', () => {
Expand All @@ -18,6 +20,27 @@ describe('buildYaml – block scalar headers', () => {
expect(yaml).toMatch(/^_m2b_molblock: \|2-$/m);
});

it('space-only title molblock uses |2- header', () => {
// no_first_name.sdf: title line is a single space, not a blank line
const yaml = buildYaml({ _m2b_molblock: SPACE_TITLE_MOLBLOCK });
expect(yaml).toMatch(/^_m2b_molblock: \|2-$/m);
});

it('multi-space title molblock uses |2- header', () => {
const yaml = buildYaml({ _m2b_molblock: MULTI_SPACE_TITLE_MOLBLOCK });
expect(yaml).toMatch(/^_m2b_molblock: \|2-$/m);
});

it('multi-space title molblock preserves title line and header line', () => {
const yaml = buildYaml({ _m2b_molblock: MULTI_SPACE_TITLE_MOLBLOCK });
const lines = yaml.split('\n');
const headerIdx = lines.findIndex((l) => l.startsWith('_m2b_molblock:'));
// Line after header: 2-space YAML indent + 3 spaces of title = 5 spaces
expect(lines[headerIdx + 1]).toBe(' ');
// Line after that: 2-space YAML indent + the OEChem header line
expect(lines[headerIdx + 2]).toBe(` ${HEADER_LINE}`);
});

it('|2- block scalar preserves the leading blank line', () => {
const yaml = buildYaml({ _m2b_molblock: BLANK_TITLE_MOLBLOCK });
// After the header line the very next line must be empty (the blank title)
Expand Down
58 changes: 58 additions & 0 deletions tests/sdf-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import { describe, it, expect } from 'vitest';
import { parseSdf } from '../src/sdf-parser';
import { buildYaml } from '../src/import-utils';

const SDF_PATH = join(__dirname, 'data', 'missing_names.sdf');
const sdfContent = readFileSync(SDF_PATH, 'utf-8');
Expand Down Expand Up @@ -77,6 +78,63 @@ describe('parseSdf – no_name_at_all.sdf', () => {
});
});

const NO_NAME_SPACES_PATH = join(__dirname, 'data', 'no_name_spaces.sdf');
const noNameSpacesContent = readFileSync(NO_NAME_SPACES_PATH, 'utf-8');

describe('parseSdf – no_name_spaces.sdf', () => {
const mols = parseSdf(noNameSpacesContent);

it('parses exactly 2 molecules', () => {
expect(mols).toHaveLength(2);
});

it('mol 1 – title line is 3 spaces (not empty, not named)', () => {
// Title line is " " (3 spaces): distinct from both blank (\n) and named titles.
// parseSdf must preserve it as-is so buildYaml can detect the whitespace-only
// first line and choose |2- over |.
const firstLine = mols[0].molblock.split('\n')[0];
expect(firstLine).toBe(' ');
});

it('mol 1 – molblock does NOT start with \\n (whitespace title, not blank title)', () => {
expect(mols[0].molblock.startsWith('\n')).toBe(false);
expect(mols[0].molblock.startsWith(' ')).toBe(true);
});

it('mol 1 – buildYaml uses |2- to prevent YAML indent auto-detection corruption', () => {
// Without |2-, YAML detects indent=4 from the first non-whitespace line
// (" -INDIGO...") and strips 4 spaces from every line, corrupting the molblock.
const yaml = buildYaml({ _m2b_molblock: mols[0].molblock });
expect(yaml).toMatch(/^_m2b_molblock: \|2-$/m);
});

it('mol 1 – molblock content is preserved correctly through buildYaml', () => {
const yaml = buildYaml({ _m2b_molblock: mols[0].molblock });
const lines = yaml.split('\n');
const headerIdx = lines.findIndex((l) => l.startsWith('_m2b_molblock:'));
// Title line: 2-space YAML indent + 3 spaces = 5 spaces
expect(lines[headerIdx + 1]).toBe(' ');
// Program line: 2-space YAML indent + " -INDIGO-..." (2 leading spaces preserved)
expect(lines[headerIdx + 2]).toBe(' -INDIGO-04142622472D');
});

it('mol 2 – blank title line (starts with \\n after leading-newline strip)', () => {
expect(mols[1].molblock.startsWith('\n')).toBe(true);
});

it('both molecules have M END in their molblock', () => {
for (const mol of mols) {
expect(mol.molblock).toContain('M END');
}
});

it('neither molecule has properties', () => {
for (const mol of mols) {
expect(Object.keys(mol.properties)).toHaveLength(0);
}
});
});

const DUP_PATH = join(__dirname, 'data', 'dup_names.sdf');
const dupContent = readFileSync(DUP_PATH, 'utf-8');

Expand Down
Loading