use crate::{
MolHandle, WASM_MAX_ATOMS, WASM_MAX_BATCH_ITEMS, WASM_MAX_JSON_STRING_BYTES,
WASM_MAX_SMARTS_MATCHES, enforce_wasm_input_len, enforce_wasm_molecule_size,
escape_json_string, json_option_string_array, json_option_u8_array, parse_smiles_json_array,
rgroup_fragment_smiles,
};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn parse_cxsmiles_json(s: &str) -> Result<String, JsValue> {
enforce_wasm_input_len("cxsmiles", s)?;
let cx = chematic_smiles::parse_cxsmiles(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
enforce_wasm_molecule_size(&cx.mol)?;
let atom_props = cx
.atom_props
.iter()
.map(|p| {
format!(
r#"{{"atom":{},"key":"{}","value":"{}"}}"#,
p.atom.0,
escape_json_string(&p.key),
escape_json_string(&p.value)
)
})
.collect::<Vec<_>>()
.join(",");
let zero_bonds = cx
.mol
.bonds()
.filter_map(|(bidx, bond)| {
(bond.order == chematic_core::BondOrder::Zero).then_some(bidx.0.to_string())
})
.collect::<Vec<_>>()
.join(",");
Ok(format!(
r#"{{"smiles":"{}","cxsmiles":"{}","atomCount":{},"bondCount":{},"atomLabels":{},"atomProps":[{}],"atomRadicals":{},"zeroBonds":[{}]}}"#,
escape_json_string(&chematic_smiles::write(&cx.mol)),
escape_json_string(&chematic_smiles::write_cxsmiles(&cx)),
cx.mol.atom_count(),
cx.mol.bond_count(),
json_option_string_array(&cx.atom_labels),
atom_props,
json_option_u8_array(&cx.atom_radicals),
zero_bonds
))
}
#[wasm_bindgen]
pub fn normalize_cxsmiles(s: &str) -> Result<String, JsValue> {
let cx = chematic_smiles::parse_cxsmiles(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
if cx.mol.atom_count() > WASM_MAX_ATOMS {
return Err(JsValue::from_str(&format!(
"Molecule exceeds maximum atom count ({} > {})",
cx.mol.atom_count(),
WASM_MAX_ATOMS
)));
}
Ok(chematic_smiles::write_cxsmiles(&cx))
}
#[wasm_bindgen]
pub fn parse_cxsmarts_json(s: &str) -> Result<String, JsValue> {
let cx = chematic_smarts::parse_cxsmarts(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
if cx.query.atom_count() > WASM_MAX_ATOMS {
return Err(JsValue::from_str(&format!(
"Query molecule exceeds maximum atom count ({} > {})",
cx.query.atom_count(),
WASM_MAX_ATOMS
)));
}
let atom_props = cx
.atom_props
.iter()
.map(|p| {
format!(
r#"{{"atom":{},"key":"{}","value":"{}"}}"#,
p.atom,
escape_json_string(&p.key),
escape_json_string(&p.value)
)
})
.collect::<Vec<_>>()
.join(",");
Ok(format!(
r#"{{"atomCount":{},"bondCount":{},"atomLabels":{},"atomProps":[{}],"atomRadicals":{}}}"#,
cx.query.atom_count(),
cx.query.bonds.len(),
json_option_string_array(&cx.atom_labels),
atom_props,
json_option_u8_array(&cx.atom_radicals)
))
}
#[wasm_bindgen]
pub fn brics_fragment_count(mol: &MolHandle) -> usize {
chematic_chem::brics_fragments(&mol.inner).len()
}
#[wasm_bindgen]
pub fn add_hydrogens(mol: &MolHandle) -> MolHandle {
MolHandle {
inner: std::rc::Rc::new(chematic_chem::add_hydrogens(&mol.inner)),
}
}
#[wasm_bindgen]
pub fn remove_hydrogens(mol: &MolHandle) -> MolHandle {
MolHandle {
inner: std::rc::Rc::new(chematic_chem::remove_hydrogens(&mol.inner)),
}
}
#[wasm_bindgen]
pub fn run_reactants(smirks: &str, reactants_smiles: &str) -> Result<String, JsValue> {
let reactant_mols: Result<Vec<chematic_core::Molecule>, _> = reactants_smiles
.split('|')
.map(|s| chematic_smiles::parse(s.trim()).map_err(|e| JsValue::from_str(&e.to_string())))
.collect();
let reactant_mols = reactant_mols?;
let refs: Vec<&chematic_core::Molecule> = reactant_mols.iter().collect();
let products = chematic_rxn::run_reactants(smirks, &refs)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let outer: Vec<String> = products
.iter()
.map(|set| {
let inner: Vec<String> = set
.iter()
.map(|mol| format!("\"{}\"", chematic_smiles::canonical_smiles(mol)))
.collect();
format!("[{}]", inner.join(", "))
})
.collect();
Ok(format!("[{}]", outer.join(", ")))
}
#[wasm_bindgen]
pub fn enumerate_library_2way(
template: &str,
scaffolds_smiles: &str,
building_blocks_smiles: &str,
) -> Result<String, JsValue> {
let scaffolds: Result<Vec<chematic_core::Molecule>, _> = scaffolds_smiles
.split('|')
.map(|s| chematic_smiles::parse(s.trim()).map_err(|e| JsValue::from_str(&e.to_string())))
.collect();
let scaffolds = scaffolds?;
let building_blocks: Result<Vec<chematic_core::Molecule>, _> = building_blocks_smiles
.split('|')
.map(|s| chematic_smiles::parse(s.trim()).map_err(|e| JsValue::from_str(&e.to_string())))
.collect();
let building_blocks = building_blocks?;
let config = chematic_rxn::LibraryConfig {
skip_failures: true,
max_size: Some(WASM_MAX_BATCH_ITEMS), };
let products =
chematic_rxn::enumerate_library_2way(template, scaffolds, building_blocks, &config)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let smiles_list: Vec<String> = products
.iter()
.map(|mol| format!("\"{}\"", chematic_smiles::canonical_smiles(mol)))
.collect();
Ok(format!("[{}]", smiles_list.join(", ")))
}
#[wasm_bindgen]
pub fn depict_reaction_svg(rxn_smiles: &str) -> Result<String, JsValue> {
let rxn =
chematic_rxn::parse_reaction(rxn_smiles).map_err(|e| JsValue::from_str(&e.to_string()))?;
const MOL_W: u32 = 200;
const MOL_H: u32 = 180;
const SEP_PLUS: u32 = 40;
const SEP_ARROW: u32 = 60;
const TOP_PAD: u32 = 20;
let opts = chematic_depict::RenderOptions {
width: Some(MOL_W),
height: Some(MOL_H),
..Default::default()
};
let mut frags: Vec<(u32, String)> = Vec::new();
let mut seps: Vec<(u32, &'static str)> = Vec::new();
let mut cursor: u32 = 0;
for (i, mol) in rxn.reactants.iter().enumerate() {
if i > 0 {
seps.push((cursor + SEP_PLUS / 2, "+"));
cursor += SEP_PLUS;
}
frags.push((cursor, chematic_depict::depict_svg_opts(mol, &opts)));
cursor += MOL_W;
}
seps.push((cursor + SEP_ARROW / 2, "→"));
cursor += SEP_ARROW;
for (i, mol) in rxn.products.iter().enumerate() {
if i > 0 {
seps.push((cursor + SEP_PLUS / 2, "+"));
cursor += SEP_PLUS;
}
frags.push((cursor, chematic_depict::depict_svg_opts(mol, &opts)));
cursor += MOL_W;
}
let total_w = cursor;
let total_h = MOL_H + TOP_PAD;
let mid_y = MOL_H / 2 + TOP_PAD;
let mut out = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{total_w}" height="{total_h}" viewBox="0 0 {total_w} {total_h}">"#
);
for (x, svg) in &frags {
out.push_str(&svg.replacen("<svg ", &format!(r#"<svg x="{x}" y="{TOP_PAD}" "#), 1));
}
for (cx, sym) in &seps {
out.push_str(&format!(
r##"<text x="{cx}" y="{mid_y}" text-anchor="middle" dominant-baseline="central" font-size="20" font-family="sans-serif" fill="#555">{sym}</text>"##
));
}
out.push_str("</svg>");
Ok(out)
}
#[wasm_bindgen]
pub fn identify_functional_groups(mol: &MolHandle) -> String {
let groups = chematic_chem::identify_functional_groups(&mol.inner);
let parts: Vec<String> = groups
.iter()
.map(|g| {
let atoms: Vec<String> = g.atom_indices.iter().map(|i| i.to_string()).collect();
format!(
"{{\"atoms\":[{}],\"type\":\"{}\"}}",
atoms.join(","),
g.atom_types
)
})
.collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn detect_functional_groups(mol: &MolHandle) -> String {
let groups = chematic_chem::detect_named_functional_groups(&mol.inner);
let parts: Vec<String> = groups
.iter()
.map(|g| {
let atoms: Vec<String> = g.atoms.iter().map(|a| a.0.to_string()).collect();
format!(
"{{\"name\":\"{}\",\"atoms\":[{}]}}",
g.name,
atoms.join(",")
)
})
.collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn murcko_scaffold(mol: &MolHandle) -> MolHandle {
let scaffold = chematic_chem::murcko_scaffold(&mol.inner);
MolHandle {
inner: std::rc::Rc::new(scaffold),
}
}
#[wasm_bindgen]
pub fn generic_murcko_scaffold(mol: &MolHandle) -> MolHandle {
let scaffold = chematic_chem::generic_murcko_scaffold(&mol.inner);
MolHandle {
inner: std::rc::Rc::new(scaffold),
}
}
#[wasm_bindgen]
pub fn canonical_tautomer(mol: &MolHandle) -> MolHandle {
let t = chematic_chem::canonical_tautomer(&mol.inner);
MolHandle {
inner: std::rc::Rc::new(t),
}
}
#[wasm_bindgen]
pub fn canonical_tautomer_with_blocked_atoms_json(
mol: &MolHandle,
blocked_atom_indices_json: &str,
) -> String {
if blocked_atom_indices_json.len() > WASM_MAX_JSON_STRING_BYTES {
return format!(
r#"{{"error":"blocked_atom_indices_json too large ({} bytes)"}}"#,
blocked_atom_indices_json.len()
);
}
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"{{"error":"molecule too large (max {} atoms)"}}"#,
WASM_MAX_ATOMS
);
}
let indices: Vec<u32> = match serde_json::from_str(blocked_atom_indices_json) {
Ok(v) => v,
Err(e) => return format!(r#"{{"error":"invalid JSON: {e}"}}"#),
};
let blocked_atoms: std::collections::HashSet<chematic_core::AtomIdx> =
indices.into_iter().map(chematic_core::AtomIdx).collect();
let config = chematic_chem::TautomerConfig {
blocked_atoms,
..chematic_chem::TautomerConfig::default()
};
let result = chematic_chem::canonical_tautomer_with_config(&mol.inner, &config);
let smi = chematic_smiles::canonical_smiles(&result);
let escaped = smi.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
#[wasm_bindgen]
pub fn enumerate_tautomers_json(mol: &MolHandle) -> String {
if mol.inner.atom_count() > WASM_MAX_ATOMS {
return format!(
r#"["{{"error":"molecule too large (max {} atoms)"}}"]"#,
WASM_MAX_ATOMS
);
}
let tautomers = chematic_chem::enumerate_tautomers(&mol.inner);
let parts: Vec<String> = tautomers
.iter()
.map(|m| {
format!(
"\"{}\"",
chematic_smiles::canonical_smiles(m).replace('"', "\\\"")
)
})
.collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn largest_fragment(mol: &MolHandle) -> MolHandle {
let frag = chematic_chem::largest_fragment(&mol.inner);
MolHandle {
inner: std::rc::Rc::new(frag),
}
}
#[wasm_bindgen]
pub fn neutralize_charges(mol: &MolHandle) -> MolHandle {
let neutral = chematic_chem::neutralize_charges(&mol.inner);
MolHandle {
inner: std::rc::Rc::new(neutral),
}
}
#[wasm_bindgen]
pub fn mcs_smiles_json(smiles_json: &str) -> Result<String, JsValue> {
let smiles_list = parse_smiles_json_array(smiles_json)?;
if smiles_list.len() < 2 {
return Err(JsValue::from_str(
"mcs_smiles_json requires at least 2 SMILES",
));
}
let mols: Vec<chematic_core::Molecule> = smiles_list
.iter()
.map(|s| {
let mol = chematic_smiles::parse(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
enforce_wasm_molecule_size(&mol)?;
Ok::<_, JsValue>(mol)
})
.collect::<Result<_, _>>()?;
let mol_refs: Vec<&chematic_core::Molecule> = mols.iter().collect();
let qmol = chematic_smarts::find_mcs(&mol_refs);
if qmol.atoms.is_empty() {
return Ok("null".to_string());
}
use chematic_core::{Atom, AtomIdx, BondOrder, Element, MoleculeBuilder};
use chematic_smarts::{AtomPrimitive, AtomQuery, BondPrimitive, BondQuery};
let mut builder = MoleculeBuilder::new();
for qa in &qmol.atoms {
let elem = match &qa.query {
AtomQuery::Primitive(AtomPrimitive::AtomicNum(n)) => {
Element::from_atomic_number(*n).unwrap_or(Element::C)
}
_ => Element::C,
};
builder.add_atom(Atom::new(elem));
}
for (atom_idx, neighbors) in qmol.adj.iter().enumerate() {
for (bond_idx, neighbor_idx) in neighbors {
if atom_idx < *neighbor_idx {
let order = match &qmol.bonds[*bond_idx].query {
BondQuery::Primitive(BondPrimitive::Double) => BondOrder::Double,
BondQuery::Primitive(BondPrimitive::Triple) => BondOrder::Triple,
BondQuery::Primitive(BondPrimitive::Aromatic) => BondOrder::Aromatic,
_ => BondOrder::Single,
};
let _ = builder.add_bond(
AtomIdx(atom_idx as u32),
AtomIdx(*neighbor_idx as u32),
order,
);
}
}
}
let mol = builder.build();
Ok(chematic_smiles::canonical_smiles(&mol))
}
#[wasm_bindgen]
pub fn mcs_smiles_json_with_ring_config(
smiles_json: &str,
ring_matches_ring_only: bool,
complete_rings_only: bool,
) -> Result<String, JsValue> {
let smiles_list = parse_smiles_json_array(smiles_json)?;
if smiles_list.len() < 2 {
return Err(JsValue::from_str(
"mcs_smiles_json_with_ring_config requires at least 2 SMILES",
));
}
let mols: Vec<chematic_core::Molecule> = smiles_list
.iter()
.map(|s| {
let mol = chematic_smiles::parse(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
enforce_wasm_molecule_size(&mol)?;
Ok::<_, JsValue>(mol)
})
.collect::<Result<_, _>>()?;
let mol_refs: Vec<&chematic_core::Molecule> = mols.iter().collect();
let config = chematic_smarts::McsConfig {
ring_matches_ring_only,
complete_rings_only,
..chematic_smarts::McsConfig::default()
};
let qmol = chematic_smarts::find_mcs_with_config(&mol_refs, &config);
if qmol.atoms.is_empty() {
return Ok("null".to_string());
}
use chematic_core::{Atom, AtomIdx, BondOrder, Element, MoleculeBuilder};
use chematic_smarts::{AtomPrimitive, AtomQuery, BondPrimitive, BondQuery};
let mut builder = MoleculeBuilder::new();
for qa in &qmol.atoms {
let elem = match &qa.query {
AtomQuery::Primitive(AtomPrimitive::AtomicNum(n)) => {
Element::from_atomic_number(*n).unwrap_or(Element::C)
}
_ => Element::C,
};
builder.add_atom(Atom::new(elem));
}
for (atom_idx, neighbors) in qmol.adj.iter().enumerate() {
for (bond_idx, neighbor_idx) in neighbors {
if atom_idx < *neighbor_idx {
let order = match &qmol.bonds[*bond_idx].query {
BondQuery::Primitive(BondPrimitive::Double) => BondOrder::Double,
BondQuery::Primitive(BondPrimitive::Triple) => BondOrder::Triple,
BondQuery::Primitive(BondPrimitive::Aromatic) => BondOrder::Aromatic,
_ => BondOrder::Single,
};
let _ = builder.add_bond(
AtomIdx(atom_idx as u32),
AtomIdx(*neighbor_idx as u32),
order,
);
}
}
}
let mol = builder.build();
Ok(chematic_smiles::canonical_smiles(&mol))
}
#[wasm_bindgen]
pub fn mmp_pairs_json(smiles_json: &str) -> Result<String, JsValue> {
let smiles_list = parse_smiles_json_array(smiles_json)?;
let mols: Vec<chematic_core::Molecule> = smiles_list
.iter()
.map(|s| {
let mol = chematic_smiles::parse(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
enforce_wasm_molecule_size(&mol)?;
Ok::<_, JsValue>(mol)
})
.collect::<Result<_, _>>()?;
let mol_refs: Vec<&chematic_core::Molecule> = mols.iter().collect();
let pairs = chematic_chem::find_mmp(&mol_refs);
let entries: Vec<String> = pairs
.iter()
.map(|p| {
format!(
r#"{{"mol_a":"{}","mol_b":"{}","core":"{}","fragment_a":"{}","fragment_b":"{}"}}"#,
escape_json_string(&p.mol_a),
escape_json_string(&p.mol_b),
escape_json_string(&p.core),
escape_json_string(&p.fragment_a),
escape_json_string(&p.fragment_b),
)
})
.collect();
Ok(format!("[{}]", entries.join(",")))
}
#[wasm_bindgen]
pub fn rgroup_decompose_json(smiles_json: &str, core_smarts: &str) -> Result<String, JsValue> {
use chematic_core::AtomIdx;
use chematic_smarts::{AtomPrimitive, AtomQuery};
use std::collections::HashSet;
let query = chematic_smarts::parse_smarts(core_smarts)
.map_err(|e| JsValue::from_str(&format!("{e:?}")))?;
let wildcard_indices: Vec<usize> = query
.atoms
.iter()
.enumerate()
.filter(|(_, qa)| matches!(&qa.query, AtomQuery::Primitive(AtomPrimitive::Wildcard)))
.map(|(i, _)| i)
.collect();
let smiles_list = parse_smiles_json_array(smiles_json)?;
let mols: Vec<chematic_core::Molecule> = smiles_list
.iter()
.map(|s| {
let mol = chematic_smiles::parse(s).map_err(|e| JsValue::from_str(&e.to_string()))?;
enforce_wasm_molecule_size(&mol)?;
Ok::<_, JsValue>(mol)
})
.collect::<Result<_, _>>()?;
let mut entries: Vec<String> = Vec::new();
for mol in &mols {
let config = chematic_smarts::MatchConfig {
max_matches: Some(WASM_MAX_SMARTS_MATCHES),
use_chirality: false,
use_isotopes: false,
uniquify: true,
max_visit_budget: None,
};
let matches = chematic_smarts::find_matches_with_config(&query, mol, &config);
if matches.is_empty() {
entries.push("{\"matched\":false}".to_string());
continue;
}
let mapping = &matches[0];
let core_atoms: HashSet<AtomIdx> = mapping
.iter()
.filter(|(qi, _)| !wildcard_indices.contains(qi))
.map(|(_, &mol_idx)| mol_idx)
.collect();
let mut rgroup_parts: Vec<String> = Vec::new();
for (rg_num, qi) in wildcard_indices.iter().enumerate() {
let smi = if let Some(&attachment) = mapping.get(qi) {
rgroup_fragment_smiles(mol, attachment, &core_atoms)
} else {
String::new()
};
rgroup_parts.push(format!(
"\"r{}\":\"{}\"",
rg_num + 1,
escape_json_string(&smi)
));
}
let rg_json = rgroup_parts.join(",");
entries.push(format!("{{\"matched\":true,{rg_json}}}"));
}
Ok(format!("[{}]", entries.join(",")))
}
#[wasm_bindgen]
pub fn normalize_reaction_smiles(rxn_smiles: &str) -> Result<String, JsValue> {
let rxn =
chematic_rxn::parse_reaction(rxn_smiles).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(chematic_rxn::write_reaction(&rxn))
}
#[wasm_bindgen]
pub fn brics_fragments_json(mol: &MolHandle) -> String {
let frags = chematic_chem::brics_fragments(&mol.inner);
let parts: Vec<String> = frags
.iter()
.map(|m| {
format!(
"\"{}\"",
escape_json_string(&chematic_smiles::canonical_smiles(m))
)
})
.collect();
format!("[{}]", parts.join(","))
}
#[wasm_bindgen]
pub fn enumerate_stereo_isomers_json(mol: &MolHandle) -> Result<String, JsValue> {
use chematic_core::{Atom, AtomIdx, BondOrder, Chirality, MoleculeBuilder};
let m = &*mol.inner;
let unspecified: Vec<AtomIdx> = m
.atoms()
.filter(|(idx, atom)| {
if atom.element.atomic_number() != 6 || atom.aromatic {
return false;
}
if atom.chirality != Chirality::None {
return false;
}
let degree = m.neighbors(*idx).count();
if degree < 2 {
return false;
}
let total = degree + chematic_core::implicit_hcount(m, *idx) as usize;
total == 4
&& m.neighbors(*idx).all(|(_, bidx)| {
!matches!(m.bond(bidx).order, BondOrder::Double | BondOrder::Triple)
})
})
.map(|(idx, _)| idx)
.collect();
let n = unspecified.len();
if n > 6 {
return Err(JsValue::from_str(&format!(
"enumerate_stereo_isomers_json: {n} unspecified centers exceeds the 6-center limit (2^{n} = {} combinations)",
1usize << n,
)));
}
if n == 0 {
let smi = chematic_smiles::canonical_smiles(m);
let inchi_str = chematic_inchi::inchi(m);
let inchikey_str = chematic_inchi::inchi_key(&inchi_str);
return Ok(format!(
r#"[{{"smiles":"{}","inchi":"{}","inchikey":"{}"}}]"#,
escape_json_string(&smi),
escape_json_string(&inchi_str),
escape_json_string(&inchikey_str),
));
}
let mut seen = std::collections::HashSet::new();
let mut results: Vec<String> = Vec::new();
for bits in 0u32..(1u32 << n) {
let chirality_overrides: std::collections::HashMap<AtomIdx, Chirality> = unspecified
.iter()
.enumerate()
.map(|(i, &idx)| {
let cw = (bits >> i) & 1 == 1;
let chirality = if cw {
Chirality::Clockwise
} else {
Chirality::CounterClockwise
};
(idx, chirality)
})
.collect();
let mut builder = MoleculeBuilder::new();
for (idx, atom) in m.atoms() {
let mut a = Atom::new(atom.element);
a.charge = atom.charge;
a.isotope = atom.isotope;
a.aromatic = atom.aromatic;
a.atom_map = atom.atom_map;
if let Some(&new_chirality) = chirality_overrides.get(&idx) {
a.chirality = new_chirality;
let implicit_h = chematic_core::implicit_hcount(m, idx);
a.hydrogen_count = Some(atom.hydrogen_count.unwrap_or(implicit_h));
} else {
a.chirality = atom.chirality;
a.hydrogen_count = atom.hydrogen_count;
}
builder.add_atom(a);
}
for (_, bond) in m.bonds() {
let _ = builder.add_bond(bond.atom1, bond.atom2, bond.order);
}
let isomer = builder.build();
let smi = chematic_smiles::canonical_smiles(&isomer);
if seen.insert(smi.clone()) {
let inchi_str = chematic_inchi::inchi(&isomer);
let inchikey_str = chematic_inchi::inchi_key(&inchi_str);
results.push(format!(
r#"{{"smiles":"{}","inchi":"{}","inchikey":"{}"}}"#,
escape_json_string(&smi),
escape_json_string(&inchi_str),
escape_json_string(&inchikey_str),
));
}
}
Ok(format!("[{}]", results.join(",")))
}
#[wasm_bindgen]
pub fn find_reaction_center_json(reaction_smiles: &str) -> String {
let rxn = match chematic_rxn::parse_reaction(reaction_smiles) {
Ok(r) => r,
Err(e) => return format!("error:{e}"),
};
let center = chematic_rxn::find_reaction_center(&rxn);
let broken: Vec<String> = center
.broken_bonds
.iter()
.map(|(a, b)| format!("[{},{}]", a.0, b.0))
.collect();
let formed: Vec<String> = center
.formed_bonds
.iter()
.map(|(a, b)| format!("[{},{}]", a.0, b.0))
.collect();
let changed: Vec<String> = center
.changed_atoms
.iter()
.map(|a| a.0.to_string())
.collect();
format!(
"{{\"broken\":[{}],\"formed\":[{}],\"changed\":[{}]}}",
broken.join(","),
formed.join(","),
changed.join(","),
)
}
#[wasm_bindgen]
pub fn standardize_smiles(smiles: &str) -> String {
let mol = match chematic_smiles::parse(smiles) {
Ok(m) => m,
Err(e) => return format!("error:{e}"),
};
let mol = chematic_chem::largest_fragment(&mol);
let mol = chematic_chem::neutralize_charges(&mol);
chematic_smiles::canonical_smiles(&mol)
}
#[wasm_bindgen]
pub fn standardize_smiles_report_json(
smiles: &str,
largest_fragment_only: bool,
neutralize_charges: bool,
remove_explicit_h: bool,
canonical_tautomer: bool,
) -> String {
let mol = match chematic_smiles::parse(smiles) {
Ok(m) => m,
Err(e) => return format!("error:{e}"),
};
let pipeline = chematic_chem::StandardizationPipeline::new(chematic_chem::StandardizeOptions {
canonical_tautomer,
neutralize_charges,
remove_explicit_h,
largest_fragment_only,
zwitterion_handling: chematic_chem::ZwitterionHandling::Normalize,
});
let (standardized, report) = pipeline.run(&mol);
let report_json = match serde_json::to_string(&report) {
Ok(json) => json,
Err(e) => return format!("error:{e}"),
};
format!(
r#"{{"smiles":"{}","report":{}}}"#,
escape_json_string(&chematic_smiles::canonical_smiles(&standardized)),
report_json
)
}
#[wasm_bindgen]
pub fn balance_check_json(reaction_smiles: &str) -> String {
let rxn = match chematic_rxn::parse_reaction(reaction_smiles) {
Ok(r) => r,
Err(e) => return format!("error:{e}"),
};
let result = chematic_rxn::balance_check(&rxn);
let diff: Vec<String> = result
.diff()
.into_iter()
.map(|s| format!("\"{}\"", s))
.collect();
format!(
"{{\"balanced\":{},\"diff\":[{}]}}",
result.balanced,
diff.join(",")
)
}
#[wasm_bindgen]
pub fn invert_stereocenter_at(mol: &MolHandle, atom_idx: u32) -> Result<MolHandle, JsValue> {
let idx = chematic_core::AtomIdx(atom_idx);
if atom_idx as usize >= mol.inner.atom_count() {
return Err(JsValue::from_str(&format!(
"atom_idx {} out of range",
atom_idx
)));
}
let new_mol = chematic_chem::invert_stereocenter(&mol.inner, idx);
Ok(MolHandle {
inner: std::rc::Rc::new(new_mol),
})
}