app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
source: the crates, flows, docs and skills of adom-aiflow 0.1.5, so PRs can be opened against them
ad411ea
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
//! A span-keeping S-expression reader for `.kicad_pcb` files: every node knows where it starts and
//! ends in the text, so the writers splice edits without re-serialising anything.
#[derive(Debug, Clone)]
pub enum Item {
Atom { text: String, start: usize, end: usize },
Node(Node),
}
#[derive(Debug, Clone)]
pub struct Node {
pub tag: String,
pub items: Vec<Item>,
pub start: usize,
pub end: usize,
}
impl Node {
/// The i-th atom (0 = the tag), unquoted.
pub fn atom(&self, i: usize) -> Option<&str> {
if i == 0 {
return Some(&self.tag);
}
match self.items.get(i - 1) {
Some(Item::Atom { text, .. }) => Some(text.as_str()),
_ => None,
}
}
pub fn number(&self, i: usize) -> Option<f64> {
self.atom(i)?.parse::<f64>().ok()
}
/// The first atom after the tag (what `(layer "F.Cu")` means).
pub fn value(&self) -> Option<&str> {
self.atom(1)
}
pub fn find(&self, tag: &str) -> Option<&Node> {
self.items.iter().find_map(|it| match it {
Item::Node(n) if n.tag == tag => Some(n),
_ => None,
})
}
pub fn find_all<'a>(&'a self, tag: &'a str) -> impl Iterator<Item = &'a Node> + 'a {
self.items.iter().filter_map(move |it| match it {
Item::Node(n) if n.tag == tag => Some(n),
_ => None,
})
}
pub fn nodes(&self) -> impl Iterator<Item = &Node> {
self.items.iter().filter_map(|it| match it {
Item::Node(n) => Some(n),
_ => None,
})
}
}
fn unescape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some(o) => out.push(o),
None => {}
}
} else {
out.push(c);
}
}
out
}
/// Parse the whole text; the root is the outer list (normally `kicad_pcb`).
pub fn parse(text: &str) -> Result<Node, String> {
let b = text.as_bytes();
let mut i = 0;
while i < b.len() && b[i].is_ascii_whitespace() {
i += 1;
}
if i >= b.len() || b[i] != b'(' {
return Err("text does not start with a list".into());
}
let (node, _) = parse_node(text, b, i)?;
Ok(node)
}
fn parse_node(text: &str, b: &[u8], start: usize) -> Result<(Node, usize), String> {
// b[start] == '('
let mut i = start + 1;
let mut items = Vec::new();
let mut tag = String::new();
let mut have_tag = false;
loop {
while i < b.len() && b[i].is_ascii_whitespace() {
i += 1;
}
if i >= b.len() {
return Err(format!("unterminated list starting at {start}"));
}
match b[i] {
b')' => {
return Ok((Node { tag, items, start, end: i + 1 }, i + 1));
}
b'(' => {
let (n, e) = parse_node(text, b, i)?;
items.push(Item::Node(n));
i = e;
}
b'"' => {
let s = i;
i += 1;
let mut raw = String::new();
while i < b.len() && b[i] != b'"' {
if b[i] == b'\\' && i + 1 < b.len() {
raw.push(b[i] as char);
raw.push(b[i + 1] as char);
i += 2;
continue;
}
// copy one UTF-8 char
let ch = text[i..].chars().next().unwrap();
raw.push(ch);
i += ch.len_utf8();
}
if i >= b.len() {
return Err(format!("unterminated string at {s}"));
}
i += 1;
let txt = unescape(&raw);
if !have_tag {
tag = txt;
have_tag = true;
} else {
items.push(Item::Atom { text: txt, start: s, end: i });
}
}
_ => {
let s = i;
while i < b.len() && !b[i].is_ascii_whitespace() && b[i] != b'(' && b[i] != b')' {
i += 1;
}
let txt = text[s..i].to_string();
if !have_tag {
tag = txt;
have_tag = true;
} else {
items.push(Item::Atom { text: txt, start: s, end: i });
}
}
}
}
}
/// KiCad's own float formatting: trimmed, at most six decimals, never "-0".
pub fn num(v: f64) -> String {
let mut s = format!("{:.6}", v);
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
if s.is_empty() || s == "-" || s == "-0" {
"0".to_string()
} else {
s
}
}
pub fn quote(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_nested_lists_with_spans() {
let t = "(kicad_pcb (version 20240108) (net 0 \"\") (footprint \"Lib:X\" (at 1 2 90) (property \"Reference\" \"R1\")))";
let n = parse(t).unwrap();
assert_eq!(n.tag, "kicad_pcb");
let fp = n.find("footprint").unwrap();
assert_eq!(fp.value(), Some("Lib:X"));
assert_eq!(fp.find("at").unwrap().number(3), Some(90.0));
assert_eq!(&t[fp.start..fp.end], "(footprint \"Lib:X\" (at 1 2 90) (property \"Reference\" \"R1\"))");
assert_eq!(num(1.5), "1.5");
assert_eq!(num(-0.0000001), "0");
assert_eq!(num(120.54), "120.54");
}
}