kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,216 @@
1
+ """
2
+ Label and hierarchical label elements parser for KiCAD schematics.
3
+
4
+ Handles parsing and serialization of Label and hierarchical label elements.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from ...core.config import config
13
+ from ..base import BaseElementParser
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LabelParser(BaseElementParser):
19
+ """Parser for Label and hierarchical label elements."""
20
+
21
+ def __init__(self):
22
+ """Initialize label parser."""
23
+ super().__init__("label")
24
+
25
+ def _parse_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
26
+ """Parse a label definition."""
27
+ # Label format: (label "text" (at x y rotation) (effects ...) (uuid ...))
28
+ if len(item) < 2:
29
+ return None
30
+
31
+ label_data = {
32
+ "text": str(item[1]), # Label text is second element
33
+ "position": {"x": 0, "y": 0},
34
+ "rotation": 0,
35
+ "size": config.defaults.font_size,
36
+ "justify_h": "left",
37
+ "justify_v": "bottom",
38
+ "uuid": None,
39
+ }
40
+
41
+ for elem in item[2:]: # Skip label keyword and text
42
+ if not isinstance(elem, list):
43
+ continue
44
+
45
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
46
+
47
+ if elem_type == "at":
48
+ # Parse position: (at x y rotation)
49
+ if len(elem) >= 3:
50
+ label_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
51
+ if len(elem) >= 4:
52
+ label_data["rotation"] = float(elem[3])
53
+
54
+ elif elem_type == "effects":
55
+ # Parse effects for font size and justification: (effects (font (size x y)) (justify left bottom))
56
+ for effect_elem in elem[1:]:
57
+ if isinstance(effect_elem, list):
58
+ effect_type = (
59
+ str(effect_elem[0])
60
+ if isinstance(effect_elem[0], sexpdata.Symbol)
61
+ else None
62
+ )
63
+
64
+ if effect_type == "font":
65
+ # Parse font size
66
+ for font_elem in effect_elem[1:]:
67
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
68
+ if len(font_elem) >= 2:
69
+ label_data["size"] = float(font_elem[1])
70
+
71
+ elif effect_type == "justify":
72
+ # Parse justification (e.g., "left bottom", "right top")
73
+ # Format: (justify left bottom) or (justify right)
74
+ if len(effect_elem) >= 2:
75
+ label_data["justify_h"] = str(effect_elem[1])
76
+ if len(effect_elem) >= 3:
77
+ label_data["justify_v"] = str(effect_elem[2])
78
+
79
+ elif elem_type == "uuid":
80
+ label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
81
+
82
+ return label_data
83
+
84
+
85
+ def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
86
+ """Parse a hierarchical label definition."""
87
+ # Format: (hierarchical_label "text" (shape input) (at x y rotation) (effects ...) (uuid ...))
88
+ if len(item) < 2:
89
+ return None
90
+
91
+ hlabel_data = {
92
+ "text": str(item[1]), # Hierarchical label text is second element
93
+ "shape": "input", # input/output/bidirectional/tri_state/passive
94
+ "position": {"x": 0, "y": 0},
95
+ "rotation": 0,
96
+ "size": config.defaults.font_size,
97
+ "justify": "left",
98
+ "uuid": None,
99
+ }
100
+
101
+ for elem in item[2:]: # Skip hierarchical_label keyword and text
102
+ if not isinstance(elem, list):
103
+ continue
104
+
105
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
106
+
107
+ if elem_type == "shape":
108
+ # Parse shape: (shape input)
109
+ if len(elem) >= 2:
110
+ hlabel_data["shape"] = str(elem[1])
111
+
112
+ elif elem_type == "at":
113
+ # Parse position: (at x y rotation)
114
+ if len(elem) >= 3:
115
+ hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
116
+ if len(elem) >= 4:
117
+ hlabel_data["rotation"] = float(elem[3])
118
+
119
+ elif elem_type == "effects":
120
+ # Parse effects for font size and justification: (effects (font (size x y)) (justify left))
121
+ for effect_elem in elem[1:]:
122
+ if isinstance(effect_elem, list):
123
+ effect_type = (
124
+ str(effect_elem[0])
125
+ if isinstance(effect_elem[0], sexpdata.Symbol)
126
+ else None
127
+ )
128
+
129
+ if effect_type == "font":
130
+ # Parse font size
131
+ for font_elem in effect_elem[1:]:
132
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
133
+ if len(font_elem) >= 2:
134
+ hlabel_data["size"] = float(font_elem[1])
135
+
136
+ elif effect_type == "justify":
137
+ # Parse justification (e.g., "left", "right")
138
+ if len(effect_elem) >= 2:
139
+ hlabel_data["justify"] = str(effect_elem[1])
140
+
141
+ elif elem_type == "uuid":
142
+ hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
143
+
144
+ return hlabel_data
145
+
146
+
147
+ def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
148
+ """Convert local label to S-expression."""
149
+ sexp = [sexpdata.Symbol("label"), label_data["text"]]
150
+
151
+ # Add position
152
+ pos = label_data["position"]
153
+ x, y = pos["x"], pos["y"]
154
+ rotation = label_data.get("rotation", 0)
155
+
156
+ # Format coordinates properly
157
+ if isinstance(x, float) and x.is_integer():
158
+ x = int(x)
159
+ if isinstance(y, float) and y.is_integer():
160
+ y = int(y)
161
+
162
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
163
+
164
+ # Add effects (font properties and justification)
165
+ size = label_data.get("size", config.defaults.font_size)
166
+ effects = [sexpdata.Symbol("effects")]
167
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
168
+ effects.append(font)
169
+
170
+ # Use justification from data, defaulting to "left bottom"
171
+ justify_h = label_data.get("justify_h", "left")
172
+ justify_v = label_data.get("justify_v", "bottom")
173
+ effects.append(
174
+ [sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
175
+ )
176
+ sexp.append(effects)
177
+
178
+ # Add UUID
179
+ if "uuid" in label_data:
180
+ sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
181
+
182
+ return sexp
183
+
184
+
185
+ def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
186
+ """Convert hierarchical label to S-expression."""
187
+ sexp = [sexpdata.Symbol("hierarchical_label"), hlabel_data["text"]]
188
+
189
+ # Add shape
190
+ shape = hlabel_data.get("shape", "input")
191
+ sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
192
+
193
+ # Add position
194
+ pos = hlabel_data["position"]
195
+ x, y = pos["x"], pos["y"]
196
+ rotation = hlabel_data.get("rotation", 0)
197
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
198
+
199
+ # Add effects (font properties)
200
+ size = hlabel_data.get("size", config.defaults.font_size)
201
+ effects = [sexpdata.Symbol("effects")]
202
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
203
+ effects.append(font)
204
+
205
+ # Use justification from data if provided, otherwise default to "left"
206
+ justify = hlabel_data.get("justify", "left")
207
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
208
+ sexp.append(effects)
209
+
210
+ # Add UUID
211
+ if "uuid" in hlabel_data:
212
+ sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
213
+
214
+ return sexp
215
+
216
+
@@ -0,0 +1,165 @@
1
+ """
2
+ Symbol library definitions parser for KiCAD schematics.
3
+
4
+ Handles parsing and serialization of Symbol library definitions.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from ..base import BaseElementParser
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LibraryParser(BaseElementParser):
18
+ """Parser for Symbol library definitions."""
19
+
20
+ def __init__(self):
21
+ """Initialize library parser."""
22
+ super().__init__("library")
23
+
24
+ def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
25
+ """Parse lib_symbols section."""
26
+ # Implementation for lib_symbols parsing
27
+ return {}
28
+
29
+ # Conversion methods from internal format to S-expression
30
+
31
+ def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
32
+ """Convert lib_symbols to S-expression."""
33
+ sexp = [sexpdata.Symbol("lib_symbols")]
34
+
35
+ # Add each symbol definition
36
+ for symbol_name, symbol_def in lib_symbols.items():
37
+ if isinstance(symbol_def, list):
38
+ # Raw S-expression data from parsed library file - use directly
39
+ sexp.append(symbol_def)
40
+ elif isinstance(symbol_def, dict):
41
+ # Dictionary format - convert to S-expression
42
+ symbol_sexp = self._create_basic_symbol_definition(symbol_name)
43
+ sexp.append(symbol_sexp)
44
+
45
+ return sexp
46
+
47
+
48
+ def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
49
+ """Create a basic symbol definition for KiCAD compatibility."""
50
+ symbol_sexp = [sexpdata.Symbol("symbol"), lib_id]
51
+
52
+ # Add basic symbol properties
53
+ symbol_sexp.extend(
54
+ [
55
+ [sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
56
+ [sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
57
+ [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
58
+ [sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
59
+ [sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")],
60
+ ]
61
+ )
62
+
63
+ # Add basic properties for the symbol
64
+ if "R" in lib_id: # Resistor
65
+ symbol_sexp.extend(
66
+ [
67
+ [
68
+ sexpdata.Symbol("property"),
69
+ "Reference",
70
+ "R",
71
+ [sexpdata.Symbol("at"), 2.032, 0, 90],
72
+ [
73
+ sexpdata.Symbol("effects"),
74
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
75
+ ],
76
+ ],
77
+ [
78
+ sexpdata.Symbol("property"),
79
+ "Value",
80
+ "R",
81
+ [sexpdata.Symbol("at"), 0, 0, 90],
82
+ [
83
+ sexpdata.Symbol("effects"),
84
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
85
+ ],
86
+ ],
87
+ [
88
+ sexpdata.Symbol("property"),
89
+ "Footprint",
90
+ "",
91
+ [sexpdata.Symbol("at"), -1.778, 0, 90],
92
+ [
93
+ sexpdata.Symbol("effects"),
94
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
95
+ [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
96
+ ],
97
+ ],
98
+ [
99
+ sexpdata.Symbol("property"),
100
+ "Datasheet",
101
+ "~",
102
+ [sexpdata.Symbol("at"), 0, 0, 0],
103
+ [
104
+ sexpdata.Symbol("effects"),
105
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
106
+ [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
107
+ ],
108
+ ],
109
+ ]
110
+ )
111
+
112
+ elif "C" in lib_id: # Capacitor
113
+ symbol_sexp.extend(
114
+ [
115
+ [
116
+ sexpdata.Symbol("property"),
117
+ "Reference",
118
+ "C",
119
+ [sexpdata.Symbol("at"), 0.635, 2.54, 0],
120
+ [
121
+ sexpdata.Symbol("effects"),
122
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
123
+ ],
124
+ ],
125
+ [
126
+ sexpdata.Symbol("property"),
127
+ "Value",
128
+ "C",
129
+ [sexpdata.Symbol("at"), 0.635, -2.54, 0],
130
+ [
131
+ sexpdata.Symbol("effects"),
132
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
133
+ ],
134
+ ],
135
+ [
136
+ sexpdata.Symbol("property"),
137
+ "Footprint",
138
+ "",
139
+ [sexpdata.Symbol("at"), 0, -1.27, 0],
140
+ [
141
+ sexpdata.Symbol("effects"),
142
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
143
+ [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
144
+ ],
145
+ ],
146
+ [
147
+ sexpdata.Symbol("property"),
148
+ "Datasheet",
149
+ "~",
150
+ [sexpdata.Symbol("at"), 0, 0, 0],
151
+ [
152
+ sexpdata.Symbol("effects"),
153
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
154
+ [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
155
+ ],
156
+ ],
157
+ ]
158
+ )
159
+
160
+ # Add basic graphics and pins (minimal for now)
161
+ symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
162
+
163
+ return symbol_sexp
164
+
165
+
@@ -0,0 +1,58 @@
1
+ """
2
+ Title block and symbol instances parser for KiCAD schematics.
3
+
4
+ Handles parsing and serialization of Title block and symbol instances.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from ..base import BaseElementParser
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MetadataParser(BaseElementParser):
18
+ """Parser for Title block and symbol instances."""
19
+
20
+ def __init__(self):
21
+ """Initialize metadata parser."""
22
+ super().__init__("metadata")
23
+
24
+ def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
25
+ """Parse title block information."""
26
+ title_block = {}
27
+ for sub_item in item[1:]:
28
+ if isinstance(sub_item, list) and len(sub_item) >= 2:
29
+ key = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
30
+ if key:
31
+ title_block[key] = sub_item[1] if len(sub_item) > 1 else None
32
+ return title_block
33
+
34
+
35
+ def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
36
+ """Parse symbol_instances section."""
37
+ # For now, just return the raw structure minus the header
38
+ return item[1:] if len(item) > 1 else []
39
+
40
+
41
+ def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
42
+ """Convert title block to S-expression."""
43
+ sexp = [sexpdata.Symbol("title_block")]
44
+
45
+ # Add standard fields
46
+ for key in ["title", "date", "rev", "company"]:
47
+ if key in title_block and title_block[key]:
48
+ sexp.append([sexpdata.Symbol(key), title_block[key]])
49
+
50
+ # Add comments with special formatting
51
+ comments = title_block.get("comments", {})
52
+ if isinstance(comments, dict):
53
+ for comment_num, comment_text in comments.items():
54
+ sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
55
+
56
+ return sexp
57
+
58
+