main
John Lauer 0.10.3: KiCad 8 numbered nets (issue 21); same-net copper pieces kept apart (issue 22) 5ed6b7b
//! A small s-expression reader for .kicad_pcb files.

#[derive(Debug, Clone)]
pub enum Sx { Atom(String), Num(f64), List(Vec<Sx>) }

impl Sx {
    pub fn list(&self) -> &[Sx] { if let Sx::List(v) = self { v } else { &[] } }
    pub fn head(&self) -> &str { match self.list().first() { Some(Sx::Atom(a)) => a, _ => "" } }
    pub fn kids(&self, key: &str) -> Vec<&Sx> { self.list().iter().filter(|k| matches!(k, Sx::List(_)) && k.head() == key).collect() }
    pub fn kid(&self, key: &str) -> Option<&Sx> { self.list().iter().find(|k| matches!(k, Sx::List(_)) && k.head() == key) }
    pub fn num(&self, i: usize) -> Option<f64> { match self.list().get(i) { Some(Sx::Num(n)) => Some(*n), Some(Sx::Atom(a)) => a.parse().ok(), _ => None } }
    /// the i-th element as text, a number or a word alike (a KiCad 8 net is `(net 41)`, its table entry `(net 41 "GND")`)
    pub fn tok(&self, i: usize) -> Option<String> { match self.list().get(i) { Some(Sx::Atom(a)) => Some(a.clone()), Some(Sx::Num(n)) => Some(if n.fract() == 0.0 { format!("{}", *n as i64) } else { format!("{n}") }), _ => None } }
    pub fn atom(&self, i: usize) -> Option<&str> { match self.list().get(i) { Some(Sx::Atom(a)) => Some(a), _ => None } }
    /// the last atom of the list (a net entry is `(net 3 "+VBAT")` or `(net "+VBAT")`)
    pub fn last_atom(&self) -> Option<&str> { self.list().iter().rev().find_map(|k| if let Sx::Atom(a) = k { Some(a.as_str()) } else { None }) }
}

pub fn parse(text: &str) -> Sx {
    let b = text.as_bytes();
    let mut stack: Vec<Vec<Sx>> = vec![Vec::new()];
    let mut i = 0;
    while i < b.len() {
        let c = b[i];
        if c == b'(' { stack.push(Vec::new()); i += 1; }
        else if c == b')' { let l = stack.pop().unwrap_or_default(); if let Some(top) = stack.last_mut() { top.push(Sx::List(l)); } i += 1; }
        else if c == b'"' {
            let mut j = i + 1; let mut s = String::new();
            while j < b.len() && b[j] != b'"' { if b[j] == b'\\' && j + 1 < b.len() { j += 1; } s.push(b[j] as char); j += 1; }
            stack.last_mut().unwrap().push(Sx::Atom(s)); i = j + 1;
        }
        else if c.is_ascii_whitespace() { i += 1; }
        else {
            let mut j = i; while j < b.len() && !b[j].is_ascii_whitespace() && b[j] != b'(' && b[j] != b')' { j += 1; }
            let t = &text[i..j];
            stack.last_mut().unwrap().push(match t.parse::<f64>() { Ok(n) if !t.is_empty() && t.bytes().next().map(|c| c.is_ascii_digit() || c == b'-' || c == b'.').unwrap_or(false) => Sx::Num(n), _ => Sx::Atom(t.to_string()) });
            i = j;
        }
    }
    stack.pop().and_then(|mut v| v.pop()).unwrap_or(Sx::List(vec![]))
}