Skip to content

Commit 6257ea8

Browse files
authored
Merge pull request #5 from asiomchen/dup-names
CSV loader improvments
2 parents 4b654e3 + cf99e49 commit 6257ea8

17 files changed

Lines changed: 169 additions & 64 deletions

docs/features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
## CSV Import
2828

2929
- Command to import CSV files via browser file picker
30-
- Auto-detects `smiles` column (case-insensitive)
31-
- Creates one note per row with YAML frontmatter
30+
- Creates one note per row with YAML frontmatter; notes are named `row_1`, `row_2`, etc.
3231
- All CSV columns added as frontmatter properties (keys sanitized to lowercase with non-alphanumeric characters replaced by `_`)
3332
- Generates a `.base` file scoped to the import folder
3433
- Handles quoted fields and escaped double quotes per RFC 4180
3534
- No RDKit dependency — SMILES are taken directly from the CSV
35+
- Select the molecule property in the view options after import
3636

3737
## Rendering Settings
3838

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default tseslint.config(
3434
},
3535
},
3636
{
37-
files: ['src/__tests__/**/*.ts'],
37+
files: ['src/__tests__/**/*.ts', 'tests/**/*.ts'],
3838
languageOptions: {
3939
globals: {
4040
...globals.node,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "mols2bases",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Molecule visualization for Bases",
55
"main": "main.js",
66
"scripts": {
77
"dev": "node esbuild.config.mjs",
88
"build": "node esbuild.config.mjs production",
9-
"lint": "eslint src/",
10-
"lint:fix": "eslint --fix src/",
9+
"lint": "eslint src/ tests/",
10+
"lint:fix": "eslint --fix src/ tests/",
1111
"format": "prettier --write src/",
1212
"format:check": "prettier --check src/",
1313
"test": "vitest run",

src/csv-import.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const BATCH_SIZE = 50;
1515

1616
export function parseCsv(text: string): { headers: string[]; rows: Record<string, string>[] } {
1717
const lines = text.split(/\r?\n/);
18-
if (lines.length === 0) return { headers: [], rows: [] };
18+
if (lines.length === 0 || lines[0].trim() === '') return { headers: [], rows: [] };
1919

2020
const headers = parseCsvRow(lines[0]);
2121
const rows: Record<string, string>[] = [];
@@ -84,19 +84,14 @@ export async function importCsv(plugin: Mols2BasesPlugin): Promise<void> {
8484

8585
try {
8686
const content = await readFileAsText(file);
87-
const { headers, rows } = parseCsv(content);
87+
const { rows } = parseCsv(content);
8888

8989
if (rows.length === 0) {
9090
notice.hide();
9191
new Notice('No data rows found in the CSV file.');
9292
return;
9393
}
9494

95-
// Auto-detect SMILES column (case-insensitive)
96-
const smilesHeader = headers.find(
97-
(h) => h.toLowerCase() === plugin.settings.csvSmilesField.toLowerCase(),
98-
);
99-
10095
notice.setMessage(`Importing ${rows.length} rows...`);
10196

10297
// Create folder based on filename
@@ -108,38 +103,19 @@ export async function importCsv(plugin: Mols2BasesPlugin): Promise<void> {
108103
}
109104

110105
// Create .base file early so the view populates as notes arrive
111-
const baseContent = buildBaseFile(
112-
`${folderPath}.base`,
113-
smilesHeader ? `note.${plugin.settings.csvSmilesField}` : undefined,
114-
);
106+
const baseContent = buildBaseFile(`${folderPath}.base`);
115107
const basePath = normalizePath(`${folderPath}.base`);
116108
const baseFile = await plugin.app.vault.create(basePath, baseContent);
117109
await plugin.app.workspace.getLeaf(false).openFile(baseFile);
118110

119-
// Show warning if no SMILES column was found
120-
if (!smilesHeader) {
121-
new Notice(
122-
`No "${plugin.settings.csvSmilesField}" column found — select the molecule property in the view options.`,
123-
5000,
124-
);
125-
}
126-
127-
// Find first non-smiles header for note naming
128-
const nameHeader = headers.find((h) => h !== smilesHeader);
129-
130111
for (let i = 0; i < rows.length; i++) {
131112
const row = rows[i];
132-
const name = (nameHeader && row[nameHeader]?.trim()) || `row_${i + 1}`;
113+
const name = `row_${i + 1}`;
133114

134-
// Build frontmatter
135115
const frontmatter: Record<string, string> = {};
136-
if (smilesHeader) {
137-
frontmatter[smilesHeader] = row[smilesHeader];
138-
}
139116
frontmatter[INTERNAL_KEYS.LINK] = `[[${baseName}.base]]`;
140117

141118
for (const [key, value] of Object.entries(row)) {
142-
if (smilesHeader && key === smilesHeader) continue;
143119
if (!frontmatter[key]) {
144120
frontmatter[key] = value;
145121
}
@@ -150,7 +126,6 @@ export async function importCsv(plugin: Mols2BasesPlugin): Promise<void> {
150126
const finalPath = await uniquePath(plugin.app, notePath);
151127
await plugin.app.vault.create(finalPath, `---\n${yaml}---\n`);
152128

153-
// Yield to the event loop after each batch
154129
if ((i + 1) % BATCH_SIZE === 0) {
155130
notice.setMessage(`Importing rows... (${i + 1} / ${rows.length})`);
156131
await sleep(0);

src/sdf-import.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,7 @@ export async function importSdf(plugin: Mols2BasesPlugin): Promise<void> {
6363
for (let i = 0; i < molecules.length; i++) {
6464
const mol = molecules[i];
6565
const molblockTitle = mol.molblock.split('\n', 1)[0].trim();
66-
const name =
67-
molblockTitle ||
68-
mol.properties.Name ||
69-
mol.properties.name ||
70-
mol.properties.COMMON_NAME ||
71-
mol.properties.ID ||
72-
mol.properties.id ||
73-
`molecule_${i + 1}`;
66+
const name = molblockTitle || `molecule_${i + 1}`;
7467

7568
// Convert MOL block to SMILES via RDKit
7669
let smiles = '';

src/settings-tab.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,5 @@ export class Mols2BasesSettingTab extends PluginSettingTab {
133133
await this.plugin.saveSettings();
134134
}),
135135
);
136-
137-
new Setting(containerEl)
138-
.setName('CSV SMILES field name')
139-
.setDesc('Name of the frontmatter property to use for SMILES when importing CSV files.')
140-
.addText((text) =>
141-
text.setValue(this.plugin.settings.csvSmilesField).onChange(async (value) => {
142-
if (value.trim()) {
143-
this.plugin.settings.csvSmilesField = value.trim();
144-
await this.plugin.saveSettings();
145-
}
146-
}),
147-
);
148136
}
149137
}

src/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export interface Mols2BasesSettings {
1616
bondLineWidth: number;
1717
transparentBg: boolean;
1818
comicMode: boolean;
19-
csvSmilesField: string;
2019
}
2120

2221
export const DEFAULT_SETTINGS: Mols2BasesSettings = {
@@ -30,7 +29,6 @@ export const DEFAULT_SETTINGS: Mols2BasesSettings = {
3029
bondLineWidth: 1.0,
3130
transparentBg: false,
3231
comicMode: false,
33-
csvSmilesField: 'smiles',
3432
};
3533

3634
export const CONFIG_KEYS = {

tests/csv-parser.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { parseCsv } from '../src/csv-import';
3+
4+
describe('parseCsv – basic parsing', () => {
5+
it('returns headers and row values', () => {
6+
const { headers, rows } = parseCsv('name,smiles\naspirin,CC(=O)Oc1ccccc1C(=O)O');
7+
expect(headers).toEqual(['name', 'smiles']);
8+
expect(rows).toHaveLength(1);
9+
expect(rows[0]).toEqual({ name: 'aspirin', smiles: 'CC(=O)Oc1ccccc1C(=O)O' });
10+
});
11+
12+
it('skips blank lines between data rows', () => {
13+
const { rows } = parseCsv('name,smiles\n\naspirin,CCO\n\n');
14+
expect(rows).toHaveLength(1);
15+
});
16+
17+
it('returns empty headers and rows for empty input', () => {
18+
const { headers, rows } = parseCsv('');
19+
expect(headers).toEqual([]);
20+
expect(rows).toHaveLength(0);
21+
});
22+
});
23+
24+
describe('parseCsv – RFC 4180 quoting', () => {
25+
it('treats a quoted field containing a comma as one field', () => {
26+
const { rows } = parseCsv('name,note\naspirin,"painkiller, fever reducer"');
27+
expect(rows[0]['note']).toBe('painkiller, fever reducer');
28+
});
29+
30+
it('unescapes doubled quotes inside a quoted field', () => {
31+
const { rows } = parseCsv('name,note\ntest,"say ""hello"""');
32+
expect(rows[0]['note']).toBe('say "hello"');
33+
});
34+
35+
it('empty trailing field is empty string', () => {
36+
const { rows } = parseCsv('a,b,c\n1,2,');
37+
expect(rows[0]['c']).toBe('');
38+
});
39+
});

tests/data/dup_names.sdf

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
compound
2+
-INDIGO-04142622472D
3+
4+
6 6 0 0 0 0 0 0 0 0999 V2000
5+
10.1348 -5.5001 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
6+
11.8652 -5.4996 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
7+
11.0016 -5.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
8+
11.8652 -6.5005 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
9+
10.1348 -6.5050 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
10+
11.0038 -7.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
11+
3 1 2 0 0 0 0
12+
1 5 1 0 0 0 0
13+
5 6 2 0 0 0 0
14+
6 4 1 0 0 0 0
15+
4 2 2 0 0 0 0
16+
2 3 1 0 0 0 0
17+
M END
18+
$$$$
19+
compound
20+
-INDIGO-04
21+
22+
6 6 0 0 0 0 0 0 0 0999 V2000
23+
10.1348 -5.5001 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
24+
11.8652 -5.4996 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
25+
11.0016 -5.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
26+
11.8652 -6.5005 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
27+
10.1348 -6.5050 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
28+
11.0038 -7.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
29+
3 1 2 0 0 0 0
30+
1 5 1 0 0 0 0
31+
5 6 2 0 0 0 0
32+
6 4 1 0 0 0 0
33+
4 2 2 0 0 0 0
34+
2 3 1 0 0 0 0
35+
M END
36+
$$$$

0 commit comments

Comments
 (0)