Skip to content
Open
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
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"d3-zoom": "^3.0.0",
"dlv": "^1.1.3",
"eventemitter3": "^5.0.4",
"expand-chemical-groups": "^1.0.0",
"fifo-logger": "^2.0.1",
"file-collection": "^6.7.0",
"file-saver": "^2.0.5",
Expand Down
7 changes: 4 additions & 3 deletions src/component/panels/MoleculesPanel/MoleculePanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
}

/**
* The moleecule pasted could be SMILES, molfile or SDF
* The molecule pasted could be SMILES, molfile, SDF or chemical groups
* @param text - text from clipboard
* @returns
*/
Expand Down Expand Up @@ -430,9 +430,10 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
/>
)}
<Toolbar.Item
tooltip="Paste SMILES or molfile"
tooltip="Paste SMILES, molfile or chemical groups"
icon={<FaPaste />}
onClick={handlePasteMoleculeAction}
data-testid="paste-molecule-action"
/>
{renderSource === 'moleculePanel' && (
<Toolbar.Item
Expand Down Expand Up @@ -502,7 +503,7 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
onDismiss={cleanShouldFallback}
onReadText={handlePasteMolecule}
text={text}
label="Enter here a molfile or SMILES"
label="Enter here a molfile, SMILES or chemical groups (e.g. HAlaGlyOH)"
/>
</PanelHeader>
);
Expand Down
33 changes: 27 additions & 6 deletions src/data/molecules/Molecule.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MoleculeView, StateMolecule } from '@zakodium/nmrium-core';
import { expandChemicalGroups } from 'expand-chemical-groups';
import { Molecule } from 'openchemlib';

import getAtomsFromMF from '../utilities/getAtomsFromMF.js';
Expand Down Expand Up @@ -36,17 +37,37 @@ type InitializeMoleculeOptions = { text: string } & Omit<
'molfile'
>;

export const parseErrorMessage =
'Failed to parse SMILES, molfile or chemical groups. Please paste a valid format';

function parseMoleculeText(text: string): Molecule {
const moleculeText = text.trim();
if (!moleculeText) {
throw new Error(parseErrorMessage);
}

if (/v[23]000/i.test(text)) {
return Molecule.fromMolfile(text);
}

try {
return Molecule.fromSmiles(moleculeText);
} catch {
// fallback to expanding chemical groups if SMILES parsing fails
// for example HAlaGlyOH, BocAlaOH ...etc.
// TODO a way to check if the text is a valid chemical group instead of fallback if can not read as SMILES
return expandChemicalGroups(moleculeText);
Comment thread
hamed-musallam marked this conversation as resolved.
}
}

export function initMolecule(
options: InitializeMoleculeOptions,
): StateMoleculeExtended {
const { id = crypto.randomUUID(), label = 'p#', text, selector } = options;
const molecule = parseMoleculeText(text);

const molecule = Molecule.fromText(text);

if (!molecule) {
throw new Error(
'Failed to parse SMILES or molfile. Please paste a valid format',
);
if (molecule.getAllAtoms() === 0) {
throw new Error(parseErrorMessage);
}
const molfile = molecule.toMolfileV3();
const mfInfo = molecule.getMolecularFormula();
Expand Down
20 changes: 11 additions & 9 deletions src/data/molecules/MoleculeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { readSDF } from '@zakodium/nmrium-core-plugins';
import { Molecule } from 'openchemlib';

import type { StateMoleculeExtended } from './Molecule.js';
import { initMolecule } from './Molecule.js';
import { initMolecule, parseErrorMessage } from './Molecule.js';

export { parseErrorMessage } from './Molecule.js';

//TODO: Move molecule initialization to nmrium-core and log an error if the molecule cannot be parsed.
export function fromJSON(
Expand Down Expand Up @@ -105,9 +107,6 @@ function getLabelNumber(reserveNumbers: number[]): number {
return 1;
}

export const parseErrorMessage =
'Failed to parse SMILES or molfile. Please paste a valid format';

function validateMolecules(molecules: StateMolecule[]) {
if (!molecules?.length) {
throw new Error(parseErrorMessage);
Expand All @@ -132,10 +131,8 @@ function validateMolecules(molecules: StateMolecule[]) {
}

/**
* We will extract the molecule(s) from the text.
* Because a molfile is actually a SDF we can just call 'readSDF' method
* @param text - text containing one or several molecules in SMILES, molfile or SDF format
* @returns
* Read SDF records and validate their structures.
* @param text - SDF content
*/

function parseSDF(text: string) {
Expand All @@ -148,12 +145,17 @@ function parseSDF(text: string) {
}
}

/**
* Parse SDF records or a single molfile, SMILES or chemical-group.
* @param text - Pasted structure text
*/
export function getMolecules(text?: string) {
if (!text?.trim()) {
throw new Error(parseErrorMessage);
}

const isSDF = /v[23]000/i.test(text);
// identifies an SDF file by checking for the presence of the '$$$$' separator
const isSDF = text.includes('$$$$');
Comment thread
hamed-musallam marked this conversation as resolved.

return isSDF ? parseSDF(text) : [initMolecule({ text })];
}
4 changes: 2 additions & 2 deletions test-e2e/panels/structures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,9 @@ test('molecules 1H spectrum', async ({ page, browserName }) => {
if (browserName !== 'webkit') {
await test.step('Check copy as molfile V3 or V2 and paste', async () => {
await clickExportMenuOption(nmrium, 'text=Copy as molfile V3');
await nmrium.clickToolByTitle('Paste SMILES or molfile');
await nmrium.page.getByTestId('paste-molecule-action').click();
await clickExportMenuOption(nmrium, 'text=Copy as molfile V2');
await nmrium.clickToolByTitle('Paste SMILES or molfile');
await nmrium.page.getByTestId('paste-molecule-action').click();

await expect(
nmrium.page.locator('_react=MoleculePanel >> text=5 / 5'),
Expand Down
Loading