app
Board to Molecule
Public Made by Adomby adom
Turn any KiCad, Fusion/EAGLE, or Altium board into an Adom molecule: wings plus MP1-MP4 machine pins, written back in the board's own format.
main
Colby Knox
board-to-molecule 1.0.0: Rust source, six before/after examples (KiCad, Fusion, Altium), Adom 3D viewer readme
eb9ea91
4d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
//! KiCad s-expression parser that keeps byte spans.
//!
//! Writers patch the original text by span so everything they do not touch stays
//! byte-identical; readers walk the tree for the preview model.
use anyhow::{bail, Result};
#[derive(Debug, Clone)]
pub enum Item {
Atom(String),
Str(String),
List(Node),
}
#[derive(Debug, Clone)]
pub struct Node {
pub items: Vec<Item>,
/// Byte range of the whole list, from '(' through ')'.
pub start: usize,
pub end: usize,
}
pub fn parse(text: &str) -> Result<Node> {
let b = text.as_bytes();
let mut stack: Vec<Node> = vec![];
let mut root: Option<Node> = None;
let mut i = 0;
while i < b.len() {
match b[i] {
b'(' => {
stack.push(Node { items: vec![], start: i, end: 0 });
i += 1;
}
b')' => {
let Some(mut n) = stack.pop() else { bail!("unbalanced ')' at byte {i}") };
n.end = i + 1;
match stack.last_mut() {
Some(p) => p.items.push(Item::List(n)),
None => {
root = Some(n);
break;
}
}
i += 1;
}
b'"' => {
let mut s = String::new();
let mut j = i + 1;
let mut seg = j;
while j < b.len() && b[j] != b'"' {
if b[j] == b'\\' && j + 1 < b.len() {
s.push_str(&text[seg..j]);
s.push(b[j + 1] as char);
j += 2;
seg = j;
} else {
j += 1;
}
}
s.push_str(&text[seg..j.min(b.len())]);
if let Some(p) = stack.last_mut() {
p.items.push(Item::Str(s));
}
i = j + 1;
}
c if c.is_ascii_whitespace() => i += 1,
_ => {
let j = b[i..]
.iter()
.position(|&c| c == b'(' || c == b')' || c == b'"' || c.is_ascii_whitespace())
.map_or(b.len(), |p| i + p);
if let Some(p) = stack.last_mut() {
p.items.push(Item::Atom(text[i..j].to_string()));
}
i = j;
}
}
}
root.ok_or_else(|| anyhow::anyhow!("no s-expression found"))
}
impl Node {
pub fn head(&self) -> &str {
match self.items.first() {
Some(Item::Atom(s)) => s,
_ => "",
}
}
pub fn lists(&self) -> impl Iterator<Item = &Node> {
self.items.iter().filter_map(|i| if let Item::List(n) = i { Some(n) } else { None })
}
pub fn all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Node> + 'a {
self.lists().filter(move |n| n.head() == name)
}
pub fn get(&self, name: &str) -> Option<&Node> {
self.lists().find(|n| n.head() == name)
}
/// The i-th value after the head, as text (atom or string).
pub fn arg(&self, i: usize) -> Option<&str> {
match self.items.get(i + 1)? {
Item::Atom(s) | Item::Str(s) => Some(s),
_ => None,
}
}
pub fn nums(&self) -> Vec<f64> {
self.items.iter().skip(1).filter_map(|i| if let Item::Atom(s) = i { s.parse().ok() } else { None }).collect()
}
/// (name x y ...) child as a point.
pub fn xy(&self, name: &str) -> Option<(f64, f64)> {
let v = self.get(name)?.nums();
(v.len() >= 2).then(|| (v[0], v[1]))
}
pub fn num(&self, name: &str) -> Option<f64> {
self.get(name)?.nums().first().copied()
}
pub fn layer(&self) -> &str {
self.get("layer").and_then(|n| n.arg(0)).unwrap_or("")
}
pub fn layers(&self) -> Vec<&str> {
match self.get("layers") {
Some(n) => n.items.iter().skip(1).filter_map(|i| match i {
Item::Atom(s) | Item::Str(s) => Some(s.as_str()),
_ => None,
}).collect(),
None => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spans_and_strings() {
let t = r#"(kicad_pcb (version 20241229) (gr_line (start 1 2) (end 3 4) (layer "Edge.Cuts")) (x "a \"q\" (b)"))"#;
let n = parse(t).unwrap();
assert_eq!(n.head(), "kicad_pcb");
let g = n.get("gr_line").unwrap();
assert_eq!(&t[g.start..g.end], r#"(gr_line (start 1 2) (end 3 4) (layer "Edge.Cuts"))"#);
assert_eq!(g.layer(), "Edge.Cuts");
assert_eq!(g.xy("end"), Some((3.0, 4.0)));
assert_eq!(n.get("x").unwrap().arg(0), Some("a \"q\" (b)"));
}
}