main
Caleb Publish 0.2.0 cbe04a0 13h ago
//! A small S-expression reader for KiCad files. Lists keep their byte span so a writer can splice
//! the original text instead of re-serialising it.

#[derive(Debug, Clone)]
pub enum Node {
    Atom(String),
    List(Vec<Node>, (usize, usize)),
}

impl Node {
    pub fn items(&self) -> &[Node] {
        match self {
            Node::List(v, _) => v,
            _ => &[],
        }
    }
    pub fn head(&self) -> &str {
        match self {
            Node::List(v, _) => match v.first() {
                Some(Node::Atom(a)) => a,
                _ => "",
            },
            _ => "",
        }
    }
    pub fn span(&self) -> (usize, usize) {
        match self {
            Node::List(_, s) => *s,
            _ => (0, 0),
        }
    }
    pub fn child(&self, name: &str) -> Option<&Node> {
        self.items().iter().find(|n| n.head() == name)
    }
    pub fn children<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Node> + 'a {
        self.items().iter().filter(move |n| n.head() == name)
    }
    pub fn atom(&self, i: usize) -> Option<&str> {
        match self.items().get(i) {
            Some(Node::Atom(a)) => Some(a),
            _ => None,
        }
    }
    pub fn num(&self, i: usize) -> Option<f64> {
        self.atom(i).and_then(|a| a.parse().ok())
    }
    pub fn atoms(&self) -> Vec<&str> {
        self.items()
            .iter()
            .filter_map(|n| if let Node::Atom(a) = n { Some(a.as_str()) } else { None })
            .collect()
    }
}

pub fn parse(text: &str) -> Result<Node, String> {
    let b = text.as_bytes();
    let mut stack: Vec<(Vec<Node>, usize)> = Vec::new();
    let mut i = 0;
    let mut root = None;
    while i < b.len() {
        match b[i] {
            b' ' | b'\t' | b'\r' | b'\n' => i += 1,
            b'(' => {
                stack.push((Vec::new(), i));
                i += 1;
            }
            b')' => {
                let (items, start) = stack.pop().ok_or("unbalanced ')'")?;
                let n = Node::List(items, (start, i + 1));
                i += 1;
                match stack.last_mut() {
                    Some((p, _)) => p.push(n),
                    None => {
                        root = Some(n);
                        break;
                    }
                }
            }
            b'"' => {
                i += 1;
                let mut s = Vec::new();
                while i < b.len() && b[i] != b'"' {
                    if b[i] == b'\\' && i + 1 < b.len() {
                        i += 1;
                        s.push(match b[i] {
                            b'n' => b'\n',
                            b't' => b'\t',
                            c => c,
                        });
                    } else {
                        s.push(b[i]);
                    }
                    i += 1;
                }
                i += 1;
                let a = Node::Atom(String::from_utf8_lossy(&s).into_owned());
                stack.last_mut().ok_or("atom outside list")?.0.push(a);
            }
            _ => {
                let st = i;
                while i < b.len() && !matches!(b[i], b' ' | b'\t' | b'\r' | b'\n' | b'(' | b')') {
                    i += 1;
                }
                let a = Node::Atom(text[st..i].to_string());
                stack.last_mut().ok_or("atom outside list")?.0.push(a);
            }
        }
    }
    root.ok_or_else(|| "no root list".to_string())
}