1use std::fs;
2use std::path::Path;
3
4use kiutils_sexpr::{parse_one, Atom, CstDocument, Node};
5
6use crate::diagnostic::Diagnostic;
7use crate::sections::{parse_paper, parse_title_block, ParsedPaper, ParsedTitleBlock};
8use crate::sexpr_edit::{
9 atom_quoted, atom_symbol, ensure_root_head_any, find_property_index, mutate_root_and_refresh,
10 paper_standard_node, paper_user_node, remove_property, upsert_node,
11 upsert_property_preserve_tail, upsert_scalar, upsert_section_child_scalar,
12};
13use crate::sexpr_utils::{
14 atom_as_f64, atom_as_string, head_of, list_child_head_count, second_atom_bool, second_atom_f64,
15 second_atom_i32, second_atom_string,
16};
17use crate::version_diag::collect_version_diagnostics;
18use crate::{Error, UnknownNode, WriteMode};
19
20#[derive(Debug, Clone, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct SchematicPaper {
23 pub kind: Option<String>,
24 pub width: Option<f64>,
25 pub height: Option<f64>,
26 pub orientation: Option<String>,
27}
28
29impl From<ParsedPaper> for SchematicPaper {
30 fn from(value: ParsedPaper) -> Self {
31 Self {
32 kind: value.kind,
33 width: value.width,
34 height: value.height,
35 orientation: value.orientation,
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct SchematicTitleBlock {
43 pub title: Option<String>,
44 pub date: Option<String>,
45 pub revision: Option<String>,
46 pub company: Option<String>,
47 pub comments: Vec<String>,
48}
49
50impl From<ParsedTitleBlock> for SchematicTitleBlock {
51 fn from(value: ParsedTitleBlock) -> Self {
52 Self {
53 title: value.title,
54 date: value.date,
55 revision: value.revision,
56 company: value.company,
57 comments: value.comments,
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct SchematicSymbolInfo {
66 pub reference: Option<String>,
67 pub lib_id: Option<String>,
68 pub value: Option<String>,
69 pub footprint: Option<String>,
70 pub properties: Vec<(String, String)>,
72}
73
74#[derive(Debug, Clone, PartialEq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct SchematicJunction {
77 pub at: Option<[f64; 2]>,
78 pub diameter: Option<f64>,
79 pub color: Option<String>,
80 pub uuid: Option<String>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct SchematicNoConnect {
86 pub at: Option<[f64; 2]>,
87 pub uuid: Option<String>,
88}
89
90#[derive(Debug, Clone, PartialEq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct SchematicWire {
93 pub points: Vec<[f64; 2]>,
94 pub uuid: Option<String>,
95 pub stroke_width: Option<f64>,
96 pub stroke_type: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct SchematicBus {
102 pub points: Vec<[f64; 2]>,
103 pub uuid: Option<String>,
104 pub stroke_width: Option<f64>,
105 pub stroke_type: Option<String>,
106}
107
108#[derive(Debug, Clone, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct SchematicBusEntry {
111 pub at: Option<[f64; 2]>,
112 pub size: Option<[f64; 2]>,
113 pub uuid: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118pub struct SchematicBusAlias {
119 pub name: Option<String>,
120 pub members: Vec<String>,
121}
122
123#[derive(Debug, Clone, PartialEq)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125pub struct SchematicNetclassFlag {
126 pub text: Option<String>,
127 pub at: Option<[f64; 2]>,
128 pub angle: Option<f64>,
129 pub uuid: Option<String>,
130}
131
132#[derive(Debug, Clone, PartialEq)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct SchematicPolyline {
135 pub points: Vec<[f64; 2]>,
136 pub uuid: Option<String>,
137}
138
139#[derive(Debug, Clone, PartialEq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct SchematicRectangle {
142 pub start: Option<[f64; 2]>,
143 pub end: Option<[f64; 2]>,
144 pub uuid: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct SchematicCircle {
150 pub center: Option<[f64; 2]>,
151 pub end: Option<[f64; 2]>,
152 pub uuid: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub struct SchematicArc {
158 pub start: Option<[f64; 2]>,
159 pub mid: Option<[f64; 2]>,
160 pub end: Option<[f64; 2]>,
161 pub uuid: Option<String>,
162}
163
164#[derive(Debug, Clone, PartialEq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166pub struct SchematicRuleArea {
167 pub name: Option<String>,
168 pub points: Vec<[f64; 2]>,
169 pub uuid: Option<String>,
170}
171
172#[derive(Debug, Clone, PartialEq)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct SchematicText {
175 pub text: Option<String>,
176 pub at: Option<[f64; 2]>,
177 pub angle: Option<f64>,
178 pub uuid: Option<String>,
179}
180
181#[derive(Debug, Clone, PartialEq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183pub struct SchematicLabel {
184 pub label_type: String,
185 pub text: Option<String>,
186 pub at: Option<[f64; 2]>,
187 pub angle: Option<f64>,
188 pub uuid: Option<String>,
189 pub shape: Option<String>,
190}
191
192#[derive(Debug, Clone, PartialEq)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194pub struct SchematicSymbol {
195 pub lib_id: Option<String>,
196 pub at: Option<[f64; 2]>,
197 pub angle: Option<f64>,
198 pub mirror: Option<String>,
199 pub unit: Option<i32>,
200 pub uuid: Option<String>,
201 pub in_bom: Option<bool>,
202 pub on_board: Option<bool>,
203 pub dnp: bool,
204 pub fields_autoplaced: bool,
205 pub properties: Vec<(String, String)>,
206 pub pin_count: usize,
207 pub reference: Option<String>,
208 pub value: Option<String>,
209}
210
211#[derive(Debug, Clone, PartialEq)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213pub struct SchematicSheet {
214 pub at: Option<[f64; 2]>,
215 pub size: Option<[f64; 2]>,
216 pub uuid: Option<String>,
217 pub fields_autoplaced: bool,
218 pub name: Option<String>,
219 pub filename: Option<String>,
220 pub pin_count: usize,
221 pub properties: Vec<(String, String)>,
222}
223
224#[derive(Debug, Clone, PartialEq)]
225#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
226pub struct SchematicImage {
227 pub at: Option<[f64; 2]>,
228 pub scale: Option<f64>,
229 pub uuid: Option<String>,
230}
231
232#[derive(Debug, Clone, PartialEq)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct SchematicSymbolInstance {
235 pub path: Option<String>,
236 pub reference: Option<String>,
237 pub unit: Option<i32>,
238 pub value: Option<String>,
239 pub footprint: Option<String>,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
244pub struct SchematicSheetInstance {
245 pub path: Option<String>,
246 pub page: Option<String>,
247}
248
249#[derive(Debug, Clone, PartialEq)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
251pub struct SchematicAst {
252 pub version: Option<i32>,
253 pub generator: Option<String>,
254 pub generator_version: Option<String>,
255 pub uuid: Option<String>,
256 pub has_paper: bool,
257 pub paper: Option<SchematicPaper>,
258 pub has_title_block: bool,
259 pub title_block: Option<SchematicTitleBlock>,
260 pub has_lib_symbols: bool,
261 pub embedded_fonts: Option<bool>,
262 pub lib_symbol_count: usize,
263 pub symbol_count: usize,
264 pub symbols: Vec<SchematicSymbol>,
265 pub sheet_count: usize,
266 pub sheets: Vec<SchematicSheet>,
267 pub junction_count: usize,
268 pub junctions: Vec<SchematicJunction>,
269 pub no_connect_count: usize,
270 pub no_connects: Vec<SchematicNoConnect>,
271 pub bus_entry_count: usize,
272 pub bus_entries: Vec<SchematicBusEntry>,
273 pub bus_alias_count: usize,
274 pub bus_aliases: Vec<SchematicBusAlias>,
275 pub wire_count: usize,
276 pub wires: Vec<SchematicWire>,
277 pub bus_count: usize,
278 pub buses: Vec<SchematicBus>,
279 pub image_count: usize,
280 pub images: Vec<SchematicImage>,
281 pub text_count: usize,
282 pub texts: Vec<SchematicText>,
283 pub text_box_count: usize,
284 pub label_count: usize,
285 pub labels: Vec<SchematicLabel>,
286 pub global_label_count: usize,
287 pub hierarchical_label_count: usize,
289 pub netclass_flag_count: usize,
291 pub netclass_flags: Vec<SchematicNetclassFlag>,
292 pub polyline_count: usize,
293 pub polylines: Vec<SchematicPolyline>,
294 pub rectangle_count: usize,
295 pub rectangles: Vec<SchematicRectangle>,
296 pub circle_count: usize,
297 pub circles: Vec<SchematicCircle>,
298 pub arc_count: usize,
299 pub arcs: Vec<SchematicArc>,
300 pub rule_area_count: usize,
301 pub rule_areas: Vec<SchematicRuleArea>,
302 pub sheet_instance_count: usize,
303 pub sheet_instances: Vec<SchematicSheetInstance>,
304 pub symbol_instance_count: usize,
305 pub symbol_instances_parsed: Vec<SchematicSymbolInstance>,
306 pub unknown_nodes: Vec<UnknownNode>,
307}
308
309#[derive(Debug, Clone)]
310pub struct SchematicDocument {
311 ast: SchematicAst,
312 cst: CstDocument,
313 diagnostics: Vec<Diagnostic>,
314 ast_dirty: bool,
315}
316
317impl SchematicDocument {
318 pub fn ast(&self) -> &SchematicAst {
319 &self.ast
320 }
321
322 pub fn ast_mut(&mut self) -> &mut SchematicAst {
323 self.ast_dirty = true;
324 &mut self.ast
325 }
326
327 pub fn cst(&self) -> &CstDocument {
328 &self.cst
329 }
330
331 pub fn diagnostics(&self) -> &[Diagnostic] {
332 &self.diagnostics
333 }
334
335 pub fn set_version(&mut self, version: i32) -> &mut Self {
336 self.mutate_root_items(|items| {
337 upsert_scalar(items, "version", atom_symbol(version.to_string()), 1)
338 })
339 }
340
341 pub fn set_generator<S: Into<String>>(&mut self, generator: S) -> &mut Self {
342 self.mutate_root_items(|items| {
343 upsert_scalar(items, "generator", atom_quoted(generator.into()), 1)
344 })
345 }
346
347 pub fn set_generator_version<S: Into<String>>(&mut self, generator_version: S) -> &mut Self {
348 self.mutate_root_items(|items| {
349 upsert_scalar(
350 items,
351 "generator_version",
352 atom_quoted(generator_version.into()),
353 1,
354 )
355 })
356 }
357
358 pub fn set_uuid<S: Into<String>>(&mut self, uuid: S) -> &mut Self {
359 self.mutate_root_items(|items| upsert_scalar(items, "uuid", atom_quoted(uuid.into()), 1))
360 }
361
362 pub fn set_paper_standard<S: Into<String>>(
363 &mut self,
364 kind: S,
365 orientation: Option<&str>,
366 ) -> &mut Self {
367 let node = paper_standard_node(kind.into(), orientation.map(|v| v.to_string()));
368 self.mutate_root_items(|items| upsert_node(items, "paper", node, 1))
369 }
370
371 pub fn set_paper_user(
372 &mut self,
373 width: f64,
374 height: f64,
375 orientation: Option<&str>,
376 ) -> &mut Self {
377 let node = paper_user_node(width, height, orientation.map(|v| v.to_string()));
378 self.mutate_root_items(|items| upsert_node(items, "paper", node, 1))
379 }
380
381 pub fn set_title<S: Into<String>>(&mut self, title: S) -> &mut Self {
382 self.mutate_root_items(|items| {
383 upsert_section_child_scalar(items, "title_block", 1, "title", atom_quoted(title.into()))
384 })
385 }
386
387 pub fn set_date<S: Into<String>>(&mut self, date: S) -> &mut Self {
388 self.mutate_root_items(|items| {
389 upsert_section_child_scalar(items, "title_block", 1, "date", atom_quoted(date.into()))
390 })
391 }
392
393 pub fn set_revision<S: Into<String>>(&mut self, revision: S) -> &mut Self {
394 self.mutate_root_items(|items| {
395 upsert_section_child_scalar(
396 items,
397 "title_block",
398 1,
399 "rev",
400 atom_quoted(revision.into()),
401 )
402 })
403 }
404
405 pub fn set_company<S: Into<String>>(&mut self, company: S) -> &mut Self {
406 self.mutate_root_items(|items| {
407 upsert_section_child_scalar(
408 items,
409 "title_block",
410 1,
411 "company",
412 atom_quoted(company.into()),
413 )
414 })
415 }
416
417 pub fn set_embedded_fonts(&mut self, enabled: bool) -> &mut Self {
418 let value = if enabled { "yes" } else { "no" };
419 self.mutate_root_items(|items| {
420 upsert_scalar(items, "embedded_fonts", atom_symbol(value.to_string()), 1)
421 })
422 }
423
424 pub fn sheet_filenames(&self) -> Vec<String> {
429 let items = match self.cst.nodes.first() {
430 Some(Node::List { items, .. }) => items,
431 _ => return Vec::new(),
432 };
433 items
434 .iter()
435 .skip(1)
436 .filter(|node| head_of(node) == Some("sheet"))
437 .filter_map(|node| {
438 let Node::List {
439 items: sheet_items, ..
440 } = node
441 else {
442 return None;
443 };
444 find_property_index(sheet_items, "Sheetfile", 1).and_then(|idx| {
446 if let Some(Node::List {
447 items: prop_items, ..
448 }) = sheet_items.get(idx)
449 {
450 prop_items.get(2).and_then(atom_as_string)
451 } else {
452 None
453 }
454 })
455 })
456 .collect()
457 }
458
459 pub fn symbol_instances(&self) -> Vec<SchematicSymbolInfo> {
461 let items = match self.cst.nodes.first() {
462 Some(Node::List { items, .. }) => items,
463 _ => return Vec::new(),
464 };
465 items
466 .iter()
467 .skip(1)
468 .filter(|node| head_of(node) == Some("symbol"))
469 .map(parse_schematic_symbol_info)
470 .collect()
471 }
472
473 pub fn upsert_symbol_instance_property<R: Into<String>, K: Into<String>, V: Into<String>>(
475 &mut self,
476 reference: R,
477 key: K,
478 value: V,
479 ) -> &mut Self {
480 let reference = reference.into();
481 let key = key.into();
482 let value = value.into();
483 self.mutate_root_items(|items| {
484 let indices = find_schematic_symbol_indices_by_reference(items, &reference);
485 let mut changed = false;
486 for idx in indices {
487 if let Some(Node::List {
488 items: sym_items, ..
489 }) = items.get_mut(idx)
490 {
491 if upsert_property_preserve_tail(sym_items, &key, &value, 1) {
492 changed = true;
493 }
494 }
495 }
496 changed
497 })
498 }
499
500 pub fn remove_symbol_instance_property<R: Into<String>, K: Into<String>>(
502 &mut self,
503 reference: R,
504 key: K,
505 ) -> &mut Self {
506 let reference = reference.into();
507 let key = key.into();
508 self.mutate_root_items(|items| {
509 let indices = find_schematic_symbol_indices_by_reference(items, &reference);
510 let mut changed = false;
511 for idx in indices {
512 if let Some(Node::List {
513 items: sym_items, ..
514 }) = items.get_mut(idx)
515 {
516 if remove_property(sym_items, &key, 1) {
517 changed = true;
518 }
519 }
520 }
521 changed
522 })
523 }
524
525 pub fn write<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
526 self.write_mode(path, WriteMode::Lossless)
527 }
528
529 pub fn write_mode<P: AsRef<Path>>(&self, path: P, mode: WriteMode) -> Result<(), Error> {
530 if self.ast_dirty {
531 return Err(Error::Validation(
532 "ast_mut changes are not serializable; use document setter APIs".to_string(),
533 ));
534 }
535 match mode {
536 WriteMode::Lossless => fs::write(path, self.cst.to_lossless_string())?,
537 WriteMode::Canonical => fs::write(path, self.cst.to_canonical_string())?,
538 }
539 Ok(())
540 }
541
542 fn mutate_root_items<F>(&mut self, mutate: F) -> &mut Self
543 where
544 F: FnOnce(&mut Vec<Node>) -> bool,
545 {
546 mutate_root_and_refresh(
547 &mut self.cst,
548 &mut self.ast,
549 &mut self.diagnostics,
550 mutate,
551 parse_ast,
552 |_cst, ast| collect_version_diagnostics(ast.version),
553 );
554 self.ast_dirty = false;
555 self
556 }
557}
558
559pub struct SchematicFile;
560
561impl SchematicFile {
562 pub fn read<P: AsRef<Path>>(path: P) -> Result<SchematicDocument, Error> {
563 let raw = fs::read_to_string(path)?;
564 let cst = parse_one(&raw)?;
565 ensure_root_head_any(&cst, &["kicad_sch"])?;
566 let ast = parse_ast(&cst);
567 let diagnostics = collect_version_diagnostics(ast.version);
568 Ok(SchematicDocument {
569 ast,
570 cst,
571 diagnostics,
572 ast_dirty: false,
573 })
574 }
575}
576
577fn find_schematic_symbol_indices_by_reference(items: &[Node], reference: &str) -> Vec<usize> {
579 items
580 .iter()
581 .enumerate()
582 .skip(1)
583 .filter(|(_, node)| {
584 if head_of(node) != Some("symbol") {
585 return false;
586 }
587 let Node::List {
588 items: sym_items, ..
589 } = node
590 else {
591 return false;
592 };
593 if let Some(prop_idx) = find_property_index(sym_items, "Reference", 1) {
594 if let Some(Node::List {
595 items: prop_items, ..
596 }) = sym_items.get(prop_idx)
597 {
598 return prop_items.get(2).and_then(atom_as_string).as_deref()
599 == Some(reference);
600 }
601 }
602 false
603 })
604 .map(|(idx, _)| idx)
605 .collect()
606}
607
608fn get_property_value(sym_items: &[Node], key: &str) -> Option<String> {
610 find_property_index(sym_items, key, 1).and_then(|idx| {
611 if let Some(Node::List {
612 items: prop_items, ..
613 }) = sym_items.get(idx)
614 {
615 prop_items.get(2).and_then(atom_as_string)
616 } else {
617 None
618 }
619 })
620}
621
622fn parse_schematic_symbol_info(node: &Node) -> SchematicSymbolInfo {
623 let Node::List { items, .. } = node else {
624 return SchematicSymbolInfo {
625 reference: None,
626 lib_id: None,
627 value: None,
628 footprint: None,
629 properties: Vec::new(),
630 };
631 };
632
633 let lib_id = items
634 .iter()
635 .skip(1)
636 .find(|n| head_of(n) == Some("lib_id"))
637 .and_then(second_atom_string);
638
639 let reference = get_property_value(items, "Reference");
640 let value = get_property_value(items, "Value");
641 let footprint = get_property_value(items, "Footprint");
642
643 let properties: Vec<(String, String)> = items
644 .iter()
645 .skip(1)
646 .filter(|n| head_of(n) == Some("property"))
647 .filter_map(|n| {
648 let Node::List {
649 items: prop_items, ..
650 } = n
651 else {
652 return None;
653 };
654 let key = prop_items.get(1).and_then(atom_as_string)?;
655 let val = prop_items
656 .get(2)
657 .and_then(atom_as_string)
658 .unwrap_or_default();
659 Some((key, val))
660 })
661 .collect();
662
663 SchematicSymbolInfo {
664 reference,
665 lib_id,
666 value,
667 footprint,
668 properties,
669 }
670}
671
672fn parse_xy2(node: &Node) -> Option<[f64; 2]> {
673 let Node::List { items, .. } = node else {
674 return None;
675 };
676 let x = items.get(1).and_then(atom_as_f64)?;
677 let y = items.get(2).and_then(atom_as_f64)?;
678 Some([x, y])
679}
680
681fn parse_at_and_angle(node: &Node) -> (Option<[f64; 2]>, Option<f64>) {
682 let Node::List { items, .. } = node else {
683 return (None, None);
684 };
685 let at = match (
686 items.get(1).and_then(atom_as_f64),
687 items.get(2).and_then(atom_as_f64),
688 ) {
689 (Some(x), Some(y)) => Some([x, y]),
690 _ => None,
691 };
692 let angle = items.get(3).and_then(atom_as_f64);
693 (at, angle)
694}
695
696fn parse_pts(node: &Node) -> Vec<[f64; 2]> {
697 let Node::List { items, .. } = node else {
698 return Vec::new();
699 };
700 items
701 .iter()
702 .skip(1)
703 .filter(|n| head_of(n) == Some("xy"))
704 .filter_map(parse_xy2)
705 .collect()
706}
707
708fn parse_property_pairs(items: &[Node]) -> Vec<(String, String)> {
709 items
710 .iter()
711 .skip(1)
712 .filter(|n| head_of(n) == Some("property"))
713 .filter_map(|n| {
714 let Node::List {
715 items: prop_items, ..
716 } = n
717 else {
718 return None;
719 };
720 let key = prop_items.get(1).and_then(atom_as_string)?;
721 let val = prop_items
722 .get(2)
723 .and_then(atom_as_string)
724 .unwrap_or_default();
725 Some((key, val))
726 })
727 .collect()
728}
729
730fn parse_stroke(node: &Node) -> (Option<f64>, Option<String>) {
731 let mut width = None;
732 let mut stroke_type = None;
733 let Node::List { items, .. } = node else {
734 return (width, stroke_type);
735 };
736 for child in items.iter().skip(1) {
737 match head_of(child) {
738 Some("width") => width = second_atom_f64(child),
739 Some("type") => stroke_type = second_atom_string(child),
740 _ => {}
741 }
742 }
743 (width, stroke_type)
744}
745
746fn parse_junction(node: &Node) -> SchematicJunction {
747 let mut at = None;
748 let mut diameter = None;
749 let mut color = None;
750 let mut uuid = None;
751 if let Node::List { items, .. } = node {
752 for child in items.iter().skip(1) {
753 match head_of(child) {
754 Some("at") => at = parse_xy2(child),
755 Some("diameter") => diameter = second_atom_f64(child),
756 Some("color") => {
757 if let Node::List {
758 items: color_items, ..
759 } = child
760 {
761 let parts: Vec<String> = color_items
762 .iter()
763 .skip(1)
764 .filter_map(atom_as_string)
765 .collect();
766 if !parts.is_empty() {
767 color = Some(parts.join(" "));
768 }
769 }
770 }
771 Some("uuid") => uuid = second_atom_string(child),
772 _ => {}
773 }
774 }
775 }
776 SchematicJunction {
777 at,
778 diameter,
779 color,
780 uuid,
781 }
782}
783
784fn parse_no_connect(node: &Node) -> SchematicNoConnect {
785 let mut at = None;
786 let mut uuid = None;
787 if let Node::List { items, .. } = node {
788 for child in items.iter().skip(1) {
789 match head_of(child) {
790 Some("at") => at = parse_xy2(child),
791 Some("uuid") => uuid = second_atom_string(child),
792 _ => {}
793 }
794 }
795 }
796 SchematicNoConnect { at, uuid }
797}
798
799fn parse_wire(node: &Node) -> SchematicWire {
800 let mut points = Vec::new();
801 let mut uuid = None;
802 let mut stroke_width = None;
803 let mut stroke_type = None;
804 if let Node::List { items, .. } = node {
805 for child in items.iter().skip(1) {
806 match head_of(child) {
807 Some("pts") => points = parse_pts(child),
808 Some("uuid") => uuid = second_atom_string(child),
809 Some("stroke") => (stroke_width, stroke_type) = parse_stroke(child),
810 _ => {}
811 }
812 }
813 }
814 SchematicWire {
815 points,
816 uuid,
817 stroke_width,
818 stroke_type,
819 }
820}
821
822fn parse_bus(node: &Node) -> SchematicBus {
823 let mut points = Vec::new();
824 let mut uuid = None;
825 let mut stroke_width = None;
826 let mut stroke_type = None;
827 if let Node::List { items, .. } = node {
828 for child in items.iter().skip(1) {
829 match head_of(child) {
830 Some("pts") => points = parse_pts(child),
831 Some("uuid") => uuid = second_atom_string(child),
832 Some("stroke") => (stroke_width, stroke_type) = parse_stroke(child),
833 _ => {}
834 }
835 }
836 }
837 SchematicBus {
838 points,
839 uuid,
840 stroke_width,
841 stroke_type,
842 }
843}
844
845fn parse_bus_entry(node: &Node) -> SchematicBusEntry {
846 let mut at = None;
847 let mut size = None;
848 let mut uuid = None;
849 if let Node::List { items, .. } = node {
850 for child in items.iter().skip(1) {
851 match head_of(child) {
852 Some("at") => at = parse_xy2(child),
853 Some("size") => size = parse_xy2(child),
854 Some("uuid") => uuid = second_atom_string(child),
855 _ => {}
856 }
857 }
858 }
859 SchematicBusEntry { at, size, uuid }
860}
861
862fn parse_bus_alias(node: &Node) -> SchematicBusAlias {
863 let mut name = None;
864 let mut members = Vec::new();
865 if let Node::List { items, .. } = node {
866 for child in items.iter().skip(1) {
867 match head_of(child) {
868 Some("name") => name = second_atom_string(child),
869 Some("members") => {
870 if let Node::List {
871 items: member_items,
872 ..
873 } = child
874 {
875 members = member_items
876 .iter()
877 .skip(1)
878 .filter_map(atom_as_string)
879 .collect();
880 }
881 }
882 _ => {}
883 }
884 }
885 }
886 SchematicBusAlias { name, members }
887}
888
889fn parse_netclass_flag(node: &Node) -> SchematicNetclassFlag {
890 let mut text = second_atom_string(node);
891 let mut at = None;
892 let mut angle = None;
893 let mut uuid = None;
894 if let Node::List { items, .. } = node {
895 for child in items.iter().skip(1) {
896 match head_of(child) {
897 Some("at") => (at, angle) = parse_at_and_angle(child),
898 Some("uuid") => uuid = second_atom_string(child),
899 _ => {}
900 }
901 }
902 if text.is_none() {
903 text = items.get(1).and_then(atom_as_string);
904 }
905 }
906 SchematicNetclassFlag {
907 text,
908 at,
909 angle,
910 uuid,
911 }
912}
913
914fn parse_polyline(node: &Node) -> SchematicPolyline {
915 let mut points = Vec::new();
916 let mut uuid = None;
917 if let Node::List { items, .. } = node {
918 for child in items.iter().skip(1) {
919 match head_of(child) {
920 Some("pts") => points = parse_pts(child),
921 Some("uuid") => uuid = second_atom_string(child),
922 _ => {}
923 }
924 }
925 }
926 SchematicPolyline { points, uuid }
927}
928
929fn parse_rectangle(node: &Node) -> SchematicRectangle {
930 let mut start = None;
931 let mut end = None;
932 let mut uuid = None;
933 if let Node::List { items, .. } = node {
934 for child in items.iter().skip(1) {
935 match head_of(child) {
936 Some("start") => start = parse_xy2(child),
937 Some("end") => end = parse_xy2(child),
938 Some("uuid") => uuid = second_atom_string(child),
939 _ => {}
940 }
941 }
942 }
943 SchematicRectangle { start, end, uuid }
944}
945
946fn parse_circle(node: &Node) -> SchematicCircle {
947 let mut center = None;
948 let mut end = None;
949 let mut uuid = None;
950 if let Node::List { items, .. } = node {
951 for child in items.iter().skip(1) {
952 match head_of(child) {
953 Some("center") => center = parse_xy2(child),
954 Some("end") => end = parse_xy2(child),
955 Some("uuid") => uuid = second_atom_string(child),
956 _ => {}
957 }
958 }
959 }
960 SchematicCircle { center, end, uuid }
961}
962
963fn parse_arc(node: &Node) -> SchematicArc {
964 let mut start = None;
965 let mut mid = None;
966 let mut end = None;
967 let mut uuid = None;
968 if let Node::List { items, .. } = node {
969 for child in items.iter().skip(1) {
970 match head_of(child) {
971 Some("start") => start = parse_xy2(child),
972 Some("mid") => mid = parse_xy2(child),
973 Some("end") => end = parse_xy2(child),
974 Some("uuid") => uuid = second_atom_string(child),
975 _ => {}
976 }
977 }
978 }
979 SchematicArc {
980 start,
981 mid,
982 end,
983 uuid,
984 }
985}
986
987fn parse_rule_area(node: &Node) -> SchematicRuleArea {
988 let mut name = None;
989 let mut points = Vec::new();
990 let mut uuid = None;
991 if let Node::List { items, .. } = node {
992 for child in items.iter().skip(1) {
993 match head_of(child) {
994 Some("name") => name = second_atom_string(child),
995 Some("pts") => points = parse_pts(child),
996 Some("uuid") => uuid = second_atom_string(child),
997 _ => {}
998 }
999 }
1000 }
1001 SchematicRuleArea { name, points, uuid }
1002}
1003
1004fn parse_text(node: &Node) -> SchematicText {
1005 let mut text = second_atom_string(node);
1006 let mut at = None;
1007 let mut angle = None;
1008 let mut uuid = None;
1009 if let Node::List { items, .. } = node {
1010 for child in items.iter().skip(1) {
1011 match head_of(child) {
1012 Some("at") => (at, angle) = parse_at_and_angle(child),
1013 Some("uuid") => uuid = second_atom_string(child),
1014 _ => {}
1015 }
1016 }
1017 if text.is_none() {
1018 text = items.get(1).and_then(atom_as_string);
1019 }
1020 }
1021 SchematicText {
1022 text,
1023 at,
1024 angle,
1025 uuid,
1026 }
1027}
1028
1029fn parse_label(node: &Node, label_type: &str) -> SchematicLabel {
1030 let mut text = second_atom_string(node);
1031 let mut at = None;
1032 let mut angle = None;
1033 let mut uuid = None;
1034 let mut shape = None;
1035 if let Node::List { items, .. } = node {
1036 for child in items.iter().skip(1) {
1037 match head_of(child) {
1038 Some("at") => (at, angle) = parse_at_and_angle(child),
1039 Some("uuid") => uuid = second_atom_string(child),
1040 Some("shape") => shape = second_atom_string(child),
1041 _ => {}
1042 }
1043 }
1044 if text.is_none() {
1045 text = items.get(1).and_then(atom_as_string);
1046 }
1047 }
1048 SchematicLabel {
1049 label_type: label_type.to_string(),
1050 text,
1051 at,
1052 angle,
1053 uuid,
1054 shape,
1055 }
1056}
1057
1058fn parse_symbol(node: &Node) -> SchematicSymbol {
1059 let mut lib_id = None;
1060 let mut at = None;
1061 let mut angle = None;
1062 let mut mirror = None;
1063 let mut unit = None;
1064 let mut uuid = None;
1065 let mut in_bom = None;
1066 let mut on_board = None;
1067 let mut dnp = false;
1068 let mut fields_autoplaced = false;
1069 let mut pin_count = 0usize;
1070 let mut reference = None;
1071 let mut value = None;
1072 let mut properties = Vec::new();
1073
1074 if let Node::List { items, .. } = node {
1075 pin_count = list_child_head_count(node, "pin");
1076 reference = get_property_value(items, "Reference");
1077 value = get_property_value(items, "Value");
1078 properties = parse_property_pairs(items);
1079
1080 for child in items.iter().skip(1) {
1081 match head_of(child) {
1082 Some("lib_id") => lib_id = second_atom_string(child),
1083 Some("at") => (at, angle) = parse_at_and_angle(child),
1084 Some("mirror") => mirror = second_atom_string(child),
1085 Some("unit") => unit = second_atom_i32(child),
1086 Some("uuid") => uuid = second_atom_string(child),
1087 Some("in_bom") => in_bom = second_atom_bool(child),
1088 Some("on_board") => on_board = second_atom_bool(child),
1089 Some("dnp") => dnp = second_atom_bool(child).unwrap_or(true),
1090 Some("fields_autoplaced") => fields_autoplaced = true,
1091 _ => {
1092 if matches!(
1093 child,
1094 Node::Atom {
1095 atom: Atom::Symbol(s),
1096 ..
1097 } if s == "fields_autoplaced"
1098 ) {
1099 fields_autoplaced = true;
1100 }
1101 }
1102 }
1103 }
1104 }
1105
1106 SchematicSymbol {
1107 lib_id,
1108 at,
1109 angle,
1110 mirror,
1111 unit,
1112 uuid,
1113 in_bom,
1114 on_board,
1115 dnp,
1116 fields_autoplaced,
1117 properties,
1118 pin_count,
1119 reference,
1120 value,
1121 }
1122}
1123
1124fn parse_sheet(node: &Node) -> SchematicSheet {
1125 let mut at = None;
1126 let mut size = None;
1127 let mut uuid = None;
1128 let mut fields_autoplaced = false;
1129 let mut pin_count = 0usize;
1130 let mut properties = Vec::new();
1131 if let Node::List { items, .. } = node {
1132 pin_count = list_child_head_count(node, "pin");
1133 properties = parse_property_pairs(items);
1134 for child in items.iter().skip(1) {
1135 match head_of(child) {
1136 Some("at") => at = parse_xy2(child),
1137 Some("size") => size = parse_xy2(child),
1138 Some("uuid") => uuid = second_atom_string(child),
1139 Some("fields_autoplaced") => fields_autoplaced = true,
1140 _ => {
1141 if matches!(
1142 child,
1143 Node::Atom {
1144 atom: Atom::Symbol(s),
1145 ..
1146 } if s == "fields_autoplaced"
1147 ) {
1148 fields_autoplaced = true;
1149 }
1150 }
1151 }
1152 }
1153 }
1154 let name = properties
1155 .iter()
1156 .find(|(k, _)| k == "Sheetname")
1157 .map(|(_, v)| v.clone());
1158 let filename = properties
1159 .iter()
1160 .find(|(k, _)| k == "Sheetfile")
1161 .map(|(_, v)| v.clone());
1162
1163 SchematicSheet {
1164 at,
1165 size,
1166 uuid,
1167 fields_autoplaced,
1168 name,
1169 filename,
1170 pin_count,
1171 properties,
1172 }
1173}
1174
1175fn parse_image(node: &Node) -> SchematicImage {
1176 let mut at = None;
1177 let mut scale = None;
1178 let mut uuid = None;
1179 if let Node::List { items, .. } = node {
1180 for child in items.iter().skip(1) {
1181 match head_of(child) {
1182 Some("at") => at = parse_xy2(child),
1183 Some("scale") => scale = second_atom_f64(child),
1184 Some("uuid") => uuid = second_atom_string(child),
1185 _ => {}
1186 }
1187 }
1188 }
1189 SchematicImage { at, scale, uuid }
1190}
1191
1192fn parse_sheet_instance(node: &Node) -> SchematicSheetInstance {
1193 let Node::List { items, .. } = node else {
1194 return SchematicSheetInstance {
1195 path: None,
1196 page: None,
1197 };
1198 };
1199
1200 let path = items.get(1).and_then(atom_as_string);
1201 let page = items
1202 .iter()
1203 .skip(2)
1204 .find(|n| head_of(n) == Some("page"))
1205 .and_then(second_atom_string);
1206
1207 SchematicSheetInstance { path, page }
1208}
1209
1210fn parse_symbol_instance(node: &Node) -> SchematicSymbolInstance {
1211 let Node::List { items, .. } = node else {
1212 return SchematicSymbolInstance {
1213 path: None,
1214 reference: None,
1215 unit: None,
1216 value: None,
1217 footprint: None,
1218 };
1219 };
1220
1221 let mut reference = None;
1222 let mut unit = None;
1223 let mut value = None;
1224 let mut footprint = None;
1225
1226 for child in items.iter().skip(2) {
1227 match head_of(child) {
1228 Some("reference") => reference = second_atom_string(child),
1229 Some("unit") => unit = second_atom_i32(child),
1230 Some("value") => value = second_atom_string(child),
1231 Some("footprint") => footprint = second_atom_string(child),
1232 _ => {}
1233 }
1234 }
1235
1236 SchematicSymbolInstance {
1237 path: items.get(1).and_then(atom_as_string),
1238 reference,
1239 unit,
1240 value,
1241 footprint,
1242 }
1243}
1244
1245fn parse_ast(cst: &CstDocument) -> SchematicAst {
1246 let mut version = None;
1247 let mut generator = None;
1248 let mut generator_version = None;
1249 let mut uuid = None;
1250 let mut has_paper = false;
1251 let mut paper = None;
1252 let mut has_title_block = false;
1253 let mut title_block = None;
1254 let mut has_lib_symbols = false;
1255 let mut embedded_fonts = None;
1256 let mut lib_symbol_count = 0usize;
1257 let mut symbol_count = 0usize;
1258 let mut symbols = Vec::new();
1259 let mut sheet_count = 0usize;
1260 let mut sheets = Vec::new();
1261 let mut junction_count = 0usize;
1262 let mut junctions = Vec::new();
1263 let mut no_connect_count = 0usize;
1264 let mut no_connects = Vec::new();
1265 let mut bus_entry_count = 0usize;
1266 let mut bus_entries = Vec::new();
1267 let mut bus_alias_count = 0usize;
1268 let mut bus_aliases = Vec::new();
1269 let mut wire_count = 0usize;
1270 let mut wires = Vec::new();
1271 let mut bus_count = 0usize;
1272 let mut buses = Vec::new();
1273 let mut image_count = 0usize;
1274 let mut images = Vec::new();
1275 let mut text_count = 0usize;
1276 let mut texts = Vec::new();
1277 let mut text_box_count = 0usize;
1278 let mut label_count = 0usize;
1279 let mut labels = Vec::new();
1280 let mut global_label_count = 0usize;
1281 let mut hierarchical_label_count = 0usize;
1282 let mut netclass_flag_count = 0usize;
1283 let mut netclass_flags = Vec::new();
1284 let mut polyline_count = 0usize;
1285 let mut polylines = Vec::new();
1286 let mut rectangle_count = 0usize;
1287 let mut rectangles = Vec::new();
1288 let mut circle_count = 0usize;
1289 let mut circles = Vec::new();
1290 let mut arc_count = 0usize;
1291 let mut arcs = Vec::new();
1292 let mut rule_area_count = 0usize;
1293 let mut rule_areas = Vec::new();
1294 let mut sheet_instance_count = 0usize;
1295 let mut sheet_instances = Vec::new();
1296 let mut symbol_instance_count = 0usize;
1297 let mut symbol_instances_parsed = Vec::new();
1298 let mut unknown_nodes = Vec::new();
1299
1300 if let Some(Node::List { items, .. }) = cst.nodes.first() {
1301 for item in items.iter().skip(1) {
1302 match head_of(item) {
1303 Some("version") => version = second_atom_i32(item),
1304 Some("generator") => generator = second_atom_string(item),
1305 Some("generator_version") => generator_version = second_atom_string(item),
1306 Some("uuid") => uuid = second_atom_string(item),
1307 Some("paper") => {
1308 has_paper = true;
1309 paper = Some(parse_paper(item).into());
1310 }
1311 Some("title_block") => {
1312 has_title_block = true;
1313 title_block = Some(parse_title_block(item).into());
1314 }
1315 Some("lib_symbols") => {
1316 has_lib_symbols = true;
1317 lib_symbol_count = list_child_head_count(item, "symbol");
1318 }
1319 Some("symbol") => {
1320 symbol_count += 1;
1321 symbols.push(parse_symbol(item));
1322 }
1323 Some("sheet") => {
1324 sheet_count += 1;
1325 sheets.push(parse_sheet(item));
1326 }
1327 Some("junction") => {
1328 junction_count += 1;
1329 junctions.push(parse_junction(item));
1330 }
1331 Some("no_connect") => {
1332 no_connect_count += 1;
1333 no_connects.push(parse_no_connect(item));
1334 }
1335 Some("bus_entry") => {
1336 bus_entry_count += 1;
1337 bus_entries.push(parse_bus_entry(item));
1338 }
1339 Some("bus_alias") => {
1340 bus_alias_count += 1;
1341 bus_aliases.push(parse_bus_alias(item));
1342 }
1343 Some("wire") => {
1344 wire_count += 1;
1345 wires.push(parse_wire(item));
1346 }
1347 Some("bus") => {
1348 bus_count += 1;
1349 buses.push(parse_bus(item));
1350 }
1351 Some("image") => {
1352 image_count += 1;
1353 images.push(parse_image(item));
1354 }
1355 Some("text") => {
1356 text_count += 1;
1357 texts.push(parse_text(item));
1358 }
1359 Some("text_box") => text_box_count += 1,
1360 Some("label") => {
1361 label_count += 1;
1362 labels.push(parse_label(item, "label"));
1363 }
1364 Some("global_label") => {
1365 global_label_count += 1;
1366 labels.push(parse_label(item, "global_label"));
1367 }
1368 Some("hierarchical_label") => {
1369 hierarchical_label_count += 1;
1370 labels.push(parse_label(item, "hierarchical_label"));
1371 }
1372 Some("netclass_flag") => {
1373 netclass_flag_count += 1;
1374 netclass_flags.push(parse_netclass_flag(item));
1375 }
1376 Some("polyline") => {
1377 polyline_count += 1;
1378 polylines.push(parse_polyline(item));
1379 }
1380 Some("rectangle") => {
1381 rectangle_count += 1;
1382 rectangles.push(parse_rectangle(item));
1383 }
1384 Some("circle") => {
1385 circle_count += 1;
1386 circles.push(parse_circle(item));
1387 }
1388 Some("arc") => {
1389 arc_count += 1;
1390 arcs.push(parse_arc(item));
1391 }
1392 Some("rule_area") => {
1393 rule_area_count += 1;
1394 rule_areas.push(parse_rule_area(item));
1395 }
1396 Some("sheet_instances") => {
1397 sheet_instance_count = list_child_head_count(item, "path");
1398 if let Node::List {
1399 items: section_items,
1400 ..
1401 } = item
1402 {
1403 sheet_instances = section_items
1404 .iter()
1405 .skip(1)
1406 .filter(|n| head_of(n) == Some("path"))
1407 .map(parse_sheet_instance)
1408 .collect();
1409 }
1410 }
1411 Some("symbol_instances") => {
1412 symbol_instance_count = list_child_head_count(item, "path");
1413 if let Node::List {
1414 items: section_items,
1415 ..
1416 } = item
1417 {
1418 symbol_instances_parsed = section_items
1419 .iter()
1420 .skip(1)
1421 .filter(|n| head_of(n) == Some("path"))
1422 .map(parse_symbol_instance)
1423 .collect();
1424 }
1425 }
1426 Some("embedded_fonts") => {
1427 embedded_fonts = second_atom_bool(item);
1428 }
1429 _ => {
1430 if let Some(unknown) = UnknownNode::from_node(item) {
1431 unknown_nodes.push(unknown);
1432 }
1433 }
1434 }
1435 }
1436 }
1437
1438 SchematicAst {
1439 version,
1440 generator,
1441 generator_version,
1442 uuid,
1443 has_paper,
1444 paper,
1445 has_title_block,
1446 title_block,
1447 has_lib_symbols,
1448 embedded_fonts,
1449 lib_symbol_count,
1450 symbol_count,
1451 symbols,
1452 sheet_count,
1453 sheets,
1454 junction_count,
1455 junctions,
1456 no_connect_count,
1457 no_connects,
1458 bus_entry_count,
1459 bus_entries,
1460 bus_alias_count,
1461 bus_aliases,
1462 wire_count,
1463 wires,
1464 bus_count,
1465 buses,
1466 image_count,
1467 images,
1468 text_count,
1469 texts,
1470 text_box_count,
1471 label_count,
1472 labels,
1473 global_label_count,
1474 hierarchical_label_count,
1475 netclass_flag_count,
1476 netclass_flags,
1477 polyline_count,
1478 polylines,
1479 rectangle_count,
1480 rectangles,
1481 circle_count,
1482 circles,
1483 arc_count,
1484 arcs,
1485 rule_area_count,
1486 rule_areas,
1487 sheet_instance_count,
1488 sheet_instances,
1489 symbol_instance_count,
1490 symbol_instances_parsed,
1491 unknown_nodes,
1492 }
1493}
1494
1495#[cfg(test)]
1496mod tests {
1497 use std::path::PathBuf;
1498 use std::time::{SystemTime, UNIX_EPOCH};
1499
1500 use super::*;
1501
1502 fn tmp_file(name: &str) -> PathBuf {
1503 let nanos = SystemTime::now()
1504 .duration_since(UNIX_EPOCH)
1505 .expect("clock")
1506 .as_nanos();
1507 std::env::temp_dir().join(format!("{name}_{nanos}.kicad_sch"))
1508 }
1509
1510 #[test]
1511 fn read_schematic_and_preserve_lossless() {
1512 let path = tmp_file("sch_read_ok");
1513 let src = "(kicad_sch (version 20250114) (generator \"eeschema\") (generator_version \"9.0\") (uuid \"u-1\") (paper \"A4\") (title_block (title \"Demo\") (date \"2026-02-25\") (comment 2 \"c2\") (comment 1 \"c1\")) (lib_symbols (symbol \"Lib:R\")) (symbol (lib_id \"Lib:R\")) (wire (pts (xy 0 0) (xy 1 1))) (sheet_instances (path \"/\" (page \"1\"))) (embedded_fonts no))\n";
1514 fs::write(&path, src).expect("write fixture");
1515
1516 let doc = SchematicFile::read(&path).expect("read");
1517 assert_eq!(doc.ast().version, Some(20250114));
1518 assert_eq!(doc.ast().generator.as_deref(), Some("eeschema"));
1519 assert_eq!(doc.ast().generator_version.as_deref(), Some("9.0"));
1520 assert_eq!(doc.ast().uuid.as_deref(), Some("u-1"));
1521 assert_eq!(
1522 doc.ast().paper.as_ref().and_then(|p| p.kind.clone()),
1523 Some("A4".to_string())
1524 );
1525 assert_eq!(doc.ast().lib_symbol_count, 1);
1526 assert_eq!(doc.ast().symbol_count, 1);
1527 assert_eq!(doc.ast().wire_count, 1);
1528 assert_eq!(doc.ast().sheet_instance_count, 1);
1529 assert_eq!(doc.ast().embedded_fonts, Some(false));
1530 assert_eq!(doc.cst().to_lossless_string(), src);
1531
1532 let _ = fs::remove_file(path);
1533 }
1534
1535 #[test]
1536 fn captures_unknown_nodes_roundtrip() {
1537 let path = tmp_file("sch_unknown");
1538 let src = "(kicad_sch (version 20250114) (generator \"eeschema\") (future_block 1 2) (symbol (lib_id \"Device:R\")))\n";
1539 fs::write(&path, src).expect("write fixture");
1540
1541 let doc = SchematicFile::read(&path).expect("read");
1542 assert_eq!(doc.ast().unknown_nodes.len(), 1);
1543
1544 let out = tmp_file("sch_unknown_out");
1545 doc.write(&out).expect("write");
1546 let got = fs::read_to_string(&out).expect("read out");
1547 assert_eq!(got, src);
1548
1549 let _ = fs::remove_file(path);
1550 let _ = fs::remove_file(out);
1551 }
1552
1553 #[test]
1554 fn edit_roundtrip_updates_core_fields() {
1555 let path = tmp_file("sch_edit");
1556 let src = "(kicad_sch (version 20241229) (generator \"eeschema\") (paper \"A4\") (title_block (title \"Old\") (date \"2025-01-01\") (rev \"A\") (company \"OldCo\")) (future_token 1 2))\n";
1557 fs::write(&path, src).expect("write fixture");
1558
1559 let mut doc = SchematicFile::read(&path).expect("read");
1560 doc.set_version(20260101)
1561 .set_generator("kiutils")
1562 .set_generator_version("dev")
1563 .set_uuid("uuid-new")
1564 .set_paper_user(297.0, 210.0, Some("landscape"))
1565 .set_title("New")
1566 .set_date("2026-02-25")
1567 .set_revision("B")
1568 .set_company("Lords")
1569 .set_embedded_fonts(true);
1570
1571 let out = tmp_file("sch_edit_out");
1572 doc.write(&out).expect("write");
1573 let reread = SchematicFile::read(&out).expect("reread");
1574
1575 assert_eq!(reread.ast().version, Some(20260101));
1576 assert_eq!(reread.ast().generator.as_deref(), Some("kiutils"));
1577 assert_eq!(reread.ast().generator_version.as_deref(), Some("dev"));
1578 assert_eq!(reread.ast().uuid.as_deref(), Some("uuid-new"));
1579 assert_eq!(
1580 reread.ast().paper.as_ref().and_then(|p| p.kind.clone()),
1581 Some("User".to_string())
1582 );
1583 assert_eq!(
1584 reread.ast().paper.as_ref().and_then(|p| p.width),
1585 Some(297.0)
1586 );
1587 assert_eq!(
1588 reread.ast().paper.as_ref().and_then(|p| p.height),
1589 Some(210.0)
1590 );
1591 assert_eq!(reread.ast().embedded_fonts, Some(true));
1592 assert_eq!(reread.ast().unknown_nodes.len(), 1);
1593 assert_eq!(
1594 reread
1595 .ast()
1596 .title_block
1597 .as_ref()
1598 .and_then(|t| t.title.clone()),
1599 Some("New".to_string())
1600 );
1601
1602 let _ = fs::remove_file(path);
1603 let _ = fs::remove_file(out);
1604 }
1605}