use crate::ast::{Column, Expr, GeometricExpr, Macro, Operator, SameKey};
use std::fmt::Write as _;
pub(crate) fn print(expr: &Expr) -> String {
let mut out = String::new();
write_expr(&mut out, expr);
out
}
fn write_expr(out: &mut String, expr: &Expr) {
match expr {
Expr::All => out.push_str("all"),
Expr::None => out.push_str("none"),
Expr::And(left, right) => binary(out, left, "and", right),
Expr::Or(left, right) => binary(out, left, "or", right),
Expr::Not(value) => prefixed(out, "not", value),
Expr::Global(value) => prefixed(out, "global", value),
Expr::ByResidue(value) => prefixed(out, "byres", value),
Expr::Same { key, target } => {
out.push_str("same ");
out.push_str(same_key(key));
out.push_str(" as ");
group(out, target);
}
Expr::Bonded { depth, target } => {
let _ = write!(out, "bonded {depth} ");
group(out, target);
}
Expr::Geometric(geometric) => write_geometric(out, geometric),
Expr::Comparison {
column,
operator,
value,
absolute,
} => {
if *absolute {
out.push_str("prop abs ");
}
let _ = write!(
out,
"{} {} {value}",
column_name(*column),
operator_symbol(*operator)
);
}
Expr::Membership { column, values } => {
out.push_str(column_name(*column));
for value in values {
out.push(' ');
membership_value(out, value);
}
}
Expr::Group(name) => {
if crate::parser::is_query_name(name) {
out.push('$');
out.push_str(name);
} else {
out.push_str("group ");
quoted(out, name);
}
}
Expr::Atom {
segment,
residue,
name,
} => {
out.push_str("atom ");
quoted(out, segment);
let _ = write!(out, " {residue} ");
quoted(out, name);
}
Expr::Macro(value) => out.push_str(macro_name(*value)),
Expr::Chirality(value) => {
out.push_str("chirality ");
quoted(out, value);
}
Expr::Smarts { source, .. } => {
out.push_str("smarts ");
quoted(out, source);
}
}
}
fn write_geometric(out: &mut String, expr: &GeometricExpr) {
match expr {
GeometricExpr::Within { radius, target } => {
let _ = write!(out, "within {radius} of ");
group(out, target);
}
GeometricExpr::Beyond { radius, target } => {
let _ = write!(out, "beyond {radius} of ");
group(out, target);
}
GeometricExpr::Around { radius, target } => {
let _ = write!(out, "around {radius} ");
group(out, target);
}
GeometricExpr::SphereZone { radius, target } => {
let _ = write!(out, "sphzone {radius} ");
group(out, target);
}
GeometricExpr::SphereLayer {
inner,
outer,
target,
} => {
let _ = write!(out, "sphlayer {inner} {outer} ");
group(out, target);
}
GeometricExpr::IsoLayer {
inner,
outer,
target,
} => {
let _ = write!(out, "isolayer {inner} {outer} ");
group(out, target);
}
GeometricExpr::CylinderZone {
radius,
z_max,
z_min,
target,
} => {
let _ = write!(out, "cyzone {radius} {z_max} {z_min} ");
group(out, target);
}
GeometricExpr::CylinderLayer {
inner,
outer,
z_max,
z_min,
target,
} => {
let _ = write!(out, "cylayer {inner} {outer} {z_max} {z_min} ");
group(out, target);
}
GeometricExpr::Point { point, radius } => {
let _ = write!(out, "point {} {} {} {radius}", point[0], point[1], point[2]);
}
}
}
fn binary(out: &mut String, left: &Expr, keyword: &str, right: &Expr) {
group(out, left);
out.push(' ');
out.push_str(keyword);
out.push(' ');
group(out, right);
}
fn prefixed(out: &mut String, keyword: &str, value: &Expr) {
out.push_str(keyword);
out.push(' ');
group(out, value);
}
fn group(out: &mut String, expr: &Expr) {
out.push('(');
write_expr(out, expr);
out.push(')');
}
fn membership_value(out: &mut String, value: &str) {
if crate::parser::is_boundary(value) {
out.push('\\');
out.push_str(value);
return;
}
if is_bare(value) {
out.push_str(value);
return;
}
if value.starts_with('\\') {
out.push('"');
out.push_str("\\\\");
escape_into(out, value);
out.push('"');
return;
}
quoted(out, value);
}
fn quoted(out: &mut String, value: &str) {
out.push('"');
escape_into(out, value);
out.push('"');
}
fn escape_into(out: &mut String, value: &str) {
for character in value.chars() {
if matches!(character, '"' | '\\') {
out.push('\\');
}
out.push(character);
}
}
fn is_bare(value: &str) -> bool {
!value.is_empty()
&& !value.bytes().any(|byte| {
byte.is_ascii_whitespace()
|| matches!(
byte,
b'(' | b')' | b'<' | b'>' | b'=' | b'!' | b'"' | b'\'' | b'\\'
)
})
}
fn operator_symbol(operator: Operator) -> &'static str {
match operator {
Operator::Less => "<",
Operator::LessEqual => "<=",
Operator::Greater => ">",
Operator::GreaterEqual => ">=",
Operator::Equal => "==",
Operator::NotEqual => "!=",
}
}
fn same_key(key: &SameKey) -> &'static str {
match key {
SameKey::Residue => "residue",
SameKey::Chain => "chain",
SameKey::Model => "model",
SameKey::Entity => "entity",
SameKey::Fragment => "fragment",
SameKey::Segment => "segment",
SameKey::Column(column) => column_name(*column),
}
}
pub(crate) const fn macro_name(value: Macro) -> &'static str {
match value {
Macro::Protein => "protein",
Macro::Backbone => "backbone",
Macro::Sidechain => "sidechain",
Macro::Nucleic => "nucleic",
Macro::NucleicBackbone => "nucleicbackbone",
Macro::NucleicBase => "nucleicbase",
Macro::NucleicSugar => "nucleicsugar",
Macro::Water => "water",
Macro::Ion => "ion",
Macro::Lipid => "lipid",
Macro::Saccharide => "saccharide",
Macro::Hetero => "hetero",
Macro::Hydrogen => "hydrogen",
Macro::Heavy => "heavy",
Macro::Polymer => "polymer",
Macro::Ligand => "ligand",
Macro::Aromatic => "aromatic",
Macro::PolarHydrogen => "polar_hydrogen",
Macro::NonpolarHydrogen => "nonpolar_hydrogen",
Macro::Helix => "helix",
Macro::Strand => "strand",
Macro::Sheet => "sheet",
Macro::AlphaHelix => "alpha_helix",
Macro::Helix310 => "helix_310",
Macro::PiHelix => "pi_helix",
Macro::Polyproline => "polyproline",
Macro::Bridge => "bridge",
Macro::Turn => "turn",
Macro::Bend => "bend",
Macro::Coil => "coil",
Macro::PlddtVeryHigh => "plddt_very_high",
Macro::PlddtConfident => "plddt_confident",
Macro::PlddtLow => "plddt_low",
Macro::PlddtVeryLow => "plddt_very_low",
}
}
pub(crate) const fn column_name(column: Column) -> &'static str {
match column {
Column::Index => "index",
Column::ByNumber => "bynum",
Column::AtomSiteId => "id",
Column::ResidueIndex => "resindex",
Column::ChainIndex => "chainindex",
Column::ModelIndex => "modelindex",
Column::Chain => "chain",
Column::LabelChain => "label_chain",
Column::AuthChain => "auth_chain",
Column::ResidueId => "resid",
Column::LabelResidueId => "label_resid",
Column::AuthResidueId => "auth_resid",
Column::ResidueName => "resname",
Column::LabelResidueName => "label_resname",
Column::AuthResidueName => "auth_resname",
Column::AtomName => "name",
Column::LabelAtomName => "label_name",
Column::AuthAtomName => "auth_name",
Column::AlternateLocation => "altloc",
Column::Model => "model",
Column::Entity => "entity",
Column::EntityType => "entity_type",
Column::Element => "element",
Column::SegmentId => "segid",
Column::InsertionCode => "icode",
Column::RecordType => "record_type",
Column::X => "x",
Column::Y => "y",
Column::Z => "z",
Column::Mass => "mass",
Column::Charge => "charge",
Column::FormalCharge => "formalcharge",
Column::Radius => "radius",
Column::BFactor => "bfactor",
Column::Occupancy => "occupancy",
Column::Plddt => "plddt",
Column::Pae => "pae",
Column::Assembly => "assembly",
}
}
#[cfg(test)]
#[path = "print_tests.rs"]
mod tests;