kicad 0.8.1 → 0.9.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 679cc2e86c98081d38458ec0503b64fc176dec709a412bcc8fcee6ff1ccc9fbc
4
- data.tar.gz: 5c4c4d2e0fa767c8c9c841f3235f7124c6f81b4aef66841e24c0e652b17dad52
3
+ metadata.gz: '097aa1cbc02ed277cface17ac18f0ee277328b17be825a7ca45d4e63bd0d7e50'
4
+ data.tar.gz: 9bd0c4a1ee66de76c8f4036d88e2e1e77f472f13ec3d0fc00ad81e52d625d8cd
5
5
  SHA512:
6
- metadata.gz: ae34e7b3091ed815b836ca0f5f3f9f73ba5c622411279122f436949bb1a9e372c5d440795ba1f494cec5f5033a51f65578405c574ff70591a3db42640d5d7011
7
- data.tar.gz: 8cf7ebfc425f27b4bd89389db45c6f3528236dd2c534b930a3102f5d06931ab383883db724d898f7eb67d518fdb5127aa384d91df74ef6fe03f59269f050d7d1
6
+ metadata.gz: 175c29e9a0d6fdd616b325c88cd5aa7e02e45735e13379356153814a8c02384486d0d998f79d4c580cf7621e07044552579547897ce32eb60bec310c5ed4622a
7
+ data.tar.gz: e42383f3dd059c3749e34ad48dd8201661930543d86ae15d50d8f4e1957be6293e7c0515b5b758d8e8233eb3132b4b1591a8548be251b8fc984de47d01854fed
data/README.md CHANGED
@@ -6,21 +6,40 @@ Parse, load, modify and rewrite Kicad (s-epression) files into a convenient tree
6
6
 
7
7
  gem install kicad
8
8
 
9
- ## Usage
9
+ ## Usage Example
10
+
11
+ For all parts with no PartNumber property, copy it from the Value field and make sure it's hidden:
10
12
 
11
13
  $ irb -r kicad
12
- irb(main):001> k = KiCad.load("my_file.kicad_sym").value
13
- irb(main):001> k.children.filter{|c| KiCad::AST::Symbol === c }.map{|c| c.values[1]}
14
- ["BC107", "CD4046"]
15
- irb(main):001> puts k.emit
14
+ irb(main):001> k = KiCad.load('mylib.kicad_sym').value
15
+ =>
16
+ #<KiCad::AST::KicadSymbolLib:0x00000001473f5488
17
+ ...
18
+ irb(main):002> k.all_symbol.each{|s| s['PartNumber'] = s.property('Value') unless s.property('PartNumber') }; nil
19
+ => nil
20
+ irb(main):003> k.all_symbol.each{|s| s.property_node('PartNumber')&.hide = true }
21
+ => nil
22
+ irb(main):001> File.open("rewrite.kicad_sym", "w") { |f| f.puts k.emit }
16
23
  ...
17
24
 
18
25
  ## Development
19
26
 
20
- After checking out the repo, run `bundle` to install dependencies. Then, run `rake spec` to run the tests.
27
+ After checking out the repo, run `bundle` to install dependencies.
21
28
 
22
29
  To install this gem onto your local machine from local source code, run `rake install`.
23
30
 
31
+ ## Resources
32
+
33
+ KiCad uses a version of the Cadence SPECCTRA Design Language, defined in https://cdn.hackaday.io/files/1666717130852064/specctra.pdf
34
+
35
+ KiCad's documentation of this is at https://dev-docs.kicad.org/en/file-formats/sexpr-intro/
36
+ The documentation is incomplete. This library contains some items as found in KiCad 9 symbol libraries,
37
+ and doesn't contain everything documented above, especially of those have not been found.
38
+ Best efforts - if you find a problem, you discovered why this is open source.
39
+ lib/kicad/asts.rb is your starting point for adding metedata.
40
+
41
+ A related Rust library that was not consulted while building this is https://github.com/adom-inc/kicad_lib/tree/main/kicad_sexpr
42
+
24
43
  ## Contributing
25
44
 
26
45
  Bug reports and pull requests are welcome on GitHub at https://github.com/cjheath/kicad-rb
data/lib/kicad/ast.rb CHANGED
@@ -11,136 +11,386 @@ module KiCad
11
11
  def emit depth = 0
12
12
  "\t"*depth +
13
13
  '(' +
14
- @values.map{|v| value(v) }*' ' +
14
+ @values.map{|v| String === v ? v.inspect : v.to_s }*' ' +
15
15
  (@children.size == 0 ? '' : "\n" + @children.map{|c| c.emit(depth+1) }*''+"\t"*depth) +
16
16
  ")\n"
17
17
  end
18
18
 
19
- def value(v)
20
- case v
21
- when ::Symbol
22
- v.to_s
23
- when String
24
- v.inspect
25
- when Float, Integer
26
- v.to_s
27
- else
28
- "Internal error"
19
+ # Define setter and getter methods for each value type this class allows
20
+ def self.value_types vts
21
+ i = 1 # @values[0] is always the class symbol
22
+ vts.each do |k, v|
23
+ # puts "#{self.name} attribute #{k.to_s} => #{v.inspect}"
24
+ # puts "attr_accessor #{self.name}.#{k} (values #{v.inspect}) is stored in @values[#{i}]"
25
+ begin
26
+ o = i # Avoid capturing i after the loop ends
27
+ define_method(:"#{k}") do
28
+ # puts "accessing #{self.class.name}.#{k} as @values[#{o}]"
29
+ @values[o]
30
+ end
31
+ define_method(:"#{k}=") do |v|
32
+ # puts "setting #{self.class.name}.#{k} as @values[#{o}]"
33
+ # REVISIT: Check valid data type matching v
34
+ @values[o] = v
35
+ end
36
+ end
37
+ i = i+1
29
38
  end
30
39
  end
40
+
41
+ def children_of_type *cts # cts is an array of AST class symbols or strings
42
+ class_names = cts.flatten.map{|k| 'KiCad::AST::'+self.class.to_class_name(k)}
43
+ @children.filter{|h| class_names.include?(h.class.name) }
44
+ end
45
+
46
+ # Define methods for each child type this class allows
47
+ def self.child_types *cts
48
+ # puts "#{self.name} allows child types #{cts.inspect}"
49
+ cts.each do |c|
50
+ if Array === c
51
+ define_method(:"all_#{c[0].to_s}") do
52
+ # puts "Looking for all [#{class_names*', '}] in #{@children.map{|q| q.class.name}.inspect}"
53
+ children_of_type(c)
54
+ end
55
+ # REVISIT: Allow deleting and adding instances to the array
56
+ # new_child = KiCad.parse('(some new node)').value
57
+ # @children.append(new_child)
58
+ else
59
+ class_name = 'KiCad::AST::'+to_class_name(c)
60
+ define_method(:"#{c}") do
61
+ # puts "Looking for first #{class_name} in #{@children.map{|q| q.class.name}.inspect}"
62
+ a = children_of_type(c)
63
+ puts "Choosing first #{self.class.name}.#{c} of #{a.size}" if a.size > 1
64
+ a.first
65
+ end
66
+ # Allow deleting this instance
67
+ define_method(:"unset_#{c.to_s}") do
68
+ child = send(:"#{c}")
69
+ @children = @children - [child] if child
70
+ child ? true : nil
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.to_class_name sym
77
+ sym.to_s.gsub(/\A[a-z]|_[a-z]/) {|from| from[-1].upcase }
78
+ end
79
+
80
+ def self.to_symbol class_name
81
+ class_name.to_s.gsub(/[A-Z]/) {|from| '_'+from[-1].downcase }.sub(/\A_/,'')
82
+ end
31
83
  end
32
84
 
33
85
  class KicadSymbolLib < Node
34
- def initialize values, children
35
- super
36
- end
86
+ child_types :version, :generator, :generator_version, [:symbol]
37
87
  end
38
88
 
39
- # Uncomment or add whatever class you need to customise:
89
+ class Generator < Node
90
+ end
40
91
 
41
- class Symbol < Node
92
+ class GeneratorVersion < Node
42
93
  end
43
94
 
44
- =begin
95
+ class Version < Node
96
+ end
97
+
98
+ # Uncomment or add whatever class you need to customise:
99
+
100
+ # Position Identifier
45
101
  class At < Node
102
+ value_types :x => Float, :y => Float, :angle => Float
46
103
  end
47
104
 
48
- class Center < Node
105
+ # Coordinate Point List
106
+ class Pts < Node
107
+ value_types({})
108
+ child_types [:xy]
49
109
  end
50
110
 
51
- class Circle < Node
111
+ class Xy < Node
112
+ value_types :x => Float, :y => Float
52
113
  end
53
114
 
54
- class Effects < Node
115
+ # Stroke Definition
116
+ class Stroke < Node
117
+ child_types :width, :type, :color
55
118
  end
56
119
 
57
- class EmbeddedFonts < Node
120
+ class Width < Node
121
+ value_types :width => Float
58
122
  end
59
123
 
60
- class End < Node
124
+ class Type < Node
125
+ value_types :type => [:dash, :dash_dot, :dash_dot_dot, :dot, :default, :solid]
61
126
  end
62
127
 
63
- class ExcludeFromSim < Node
128
+ class Color < Node
129
+ value_types :r => Integer, :g => Integer, :b => Integer, :a => Integer
64
130
  end
65
131
 
66
- class Fill < Node
132
+ # Text Effects
133
+ class Effects < Node
134
+ child_types :font, :justify, :hide
67
135
  end
68
136
 
69
137
  class Font < Node
138
+ child_types :face, :size, :thickness, :bold, :italic, :line_spacing
70
139
  end
71
140
 
72
- class Generator < Node
141
+ class Face < Node
142
+ value_types :face_name => String
73
143
  end
74
144
 
75
- class GeneratorVersion < Node
145
+ class Size < Node
146
+ value_types :height => Float, :width => Float
76
147
  end
77
148
 
78
- class Hide < Node
149
+ class Thickness < Node
150
+ value_types :thickness => Float
79
151
  end
80
152
 
81
- class InBom < Node
153
+ class Bold < Node
154
+ value_types :bold => [:no, :yes]
82
155
  end
83
156
 
84
- class Length < Node
157
+ class Italic < Node
158
+ value_types :bold => [:no, :yes]
85
159
  end
86
160
 
87
- class Name < Node
161
+ class LineSpacing < Node
162
+ value_types :line_spacing => Float
88
163
  end
89
164
 
90
- class Number < Node
165
+ class Justify < Node
166
+ value_types :justify => [[:right, :left, :top, :bottom, :mirror]] # Can have multiple
91
167
  end
92
168
 
93
- class Offset < Node
169
+ class Hide < Node
170
+ value_types :hide => [:no, :yes]
94
171
  end
95
172
 
96
- class OnBoard < Node
173
+ # Page Settings
174
+ class Paper < Node
175
+ # REVISIT: Either paper_size or width/height
176
+ value_types :paper_size => [:A0, :A1, :A2, :A3, :A4, :A5, :A, :B, :C, :D, :E],
177
+ :width => Float, :height => Float,
178
+ :portrait => [:portrait]
97
179
  end
98
180
 
99
- class Pin < Node
181
+ # Title Block
182
+ class TitleBlock < Node
183
+ child_types :title, :date, :rev, :company, # All take one string as value
184
+ [:comment] # N (1..9) and String # REVISIT: Implement Comment
100
185
  end
101
186
 
102
- class PinNames < Node
187
+ # Properties
188
+ class Property < Node
189
+ value_types :key => String, :value => String
190
+ child_types :at, :effects
191
+
192
+ # Set or clear (hide) on the property_node
193
+ def hide=(h = true)
194
+ v = (h ? :yes : :no)
195
+ if !effects # No effects yet
196
+ prop = KiCad.parse(%Q{(effects(hide #{v}))})&.value
197
+ @children.append(prop) if prop
198
+ elsif (existing = effects.hide) # Effects and hide already
199
+ existing.hide = v
200
+ else # Create new (hide) node
201
+ prop = KiCad.parse(%Q{(hide #{v})})&.value
202
+ @children.append(prop) if prop
203
+ end
204
+ end
103
205
  end
104
206
 
105
- class PinNumbers < Node
207
+ # Universally Unique Identifier
208
+ class Uuid < Node
209
+ value_types :uuid => String
106
210
  end
107
211
 
108
- class Polyline < Node
212
+ # Images
213
+ class Image < Node
214
+ child_types :at, :scale, :layer, :uuid, :data
109
215
  end
110
216
 
111
- class Property < Node
217
+ class Data < Node
218
+ value_types :data => String # REVISIT: Base64 data - not encoded as a string I think?
112
219
  end
113
220
 
114
- class Pts < Node
221
+ class Circle < Node
222
+ child_types :center, :radius, :stroke, :fill
223
+ end
224
+
225
+ class Center < Xy
226
+ # value_types :x => Float, :y => Float
115
227
  end
116
228
 
117
229
  class Radius < Node
230
+ value_types :radius => Float
231
+ end
232
+
233
+ class Fill < Node
234
+ value_types :fill => [:no, :yes]
235
+ end
236
+
237
+ class Arc < Node
238
+ child_types :start, :mid, :end, :stroke, :fill
239
+ end
240
+
241
+ class Start < Xy
242
+ # value_types :x => Float, :y => Float
243
+ end
244
+
245
+ class Mid < Xy
246
+ # value_types :x => Float, :y => Float
247
+ end
248
+
249
+ class End < Xy
250
+ # value_types :x => Float, :y => Float
118
251
  end
119
252
 
120
253
  class Rectangle < Node
254
+ child_types :start, :end, :stroke, :fill
121
255
  end
122
256
 
123
- class Size < Node
257
+ class Polyline < Node
258
+ child_types :pts, :stroke, :fill
124
259
  end
125
260
 
126
- class Start < Node
261
+ class Text < Node
262
+ value_types :text => String
263
+ child_types :at, :effects
127
264
  end
128
265
 
129
- class Stroke < Node
266
+ class Offset < Xy
267
+ # value_types :x => Float, :y => Float
130
268
  end
131
269
 
132
- class Type < Node
270
+ # Symbol Pins
271
+ class Pin < Node
272
+ value_types :electrical_type => [
273
+ :input, # Pin is an input.
274
+ :output, # Pin is an output.
275
+ :bidirectional, # Pin can be both input and output.
276
+ :tri_state, # Pin is a tri-state output.
277
+ :passive, # Pin is electrically passive.
278
+ :free, # Not internally connected.
279
+ :unspecified, # Pin does not have a specified electrical type.
280
+ :power_in, # Pin is a power input.
281
+ :power_out, # Pin is a power output.
282
+ :open_collector, # Pin is an open collector output.
283
+ :open_emitter, # Pin is an open emitter output.
284
+ :no_connect, # Pin has no electrical connection.
285
+ ], :graphic_style => [
286
+ :line,
287
+ :inverted,
288
+ :clock,
289
+ :inverted_clock,
290
+ :input_low,
291
+ :clock_low,
292
+ :output_low,
293
+ :edge_clock_high,
294
+ :non_logic
295
+ ]
296
+ child_types :length, :name, :number
133
297
  end
134
298
 
135
- class Version < Node
299
+ class Length < Node
300
+ value_types :length => Float
136
301
  end
137
302
 
138
- class Width < Node
303
+ class Name < Node
304
+ value_types :name => String
305
+ child_types :effects
139
306
  end
140
307
 
141
- class Xy < Node
308
+ class Number < Node
309
+ value_types :number => String
310
+ child_types :effects
311
+ end
312
+
313
+ class Polygon < Node
314
+ child_types :pts, :stroke, :fill
315
+ end
316
+
317
+ # Symbols
318
+ class Symbol < Node
319
+ value_types :id => String
320
+ child_types :extends, :exclude_from_sym, :pin_numbers, :pin_names, :in_bom, :on_board,
321
+ [:property],
322
+ [:shape, :circle, :rectangle, :text, :polyline, :arc], # REVISIT: Lots more types of graphic items here...
323
+ [:pin],
324
+ [:symbol], # Child symbols (units) embedded in a parent
325
+ :unit_name,
326
+ :embedded_fonts
327
+
328
+ # Get or set Property values by key, or as a Hash
329
+ def property_node k
330
+ self.all_property.detect{|p| p.key == k}
331
+ end
332
+
333
+ def property k = nil
334
+ if k
335
+ property_node(k)&.value
336
+ else # Return all properties as a Hash
337
+ Hash[*self.all_property.map{|p| [p.key, p.value] }.flatten]
338
+ end
339
+ end
340
+
341
+ def [](k)
342
+ property[k]
343
+ end
344
+
345
+ def []=(k, v)
346
+ # puts "Setting property #{k} to #{v.inspect}"
347
+ if (prop = property_node(k))
348
+ prop.value = v
349
+ else # Create new Property using the parser:
350
+ prop = KiCad.parse(%Q{
351
+ (property "#{k}" #{String === v ? v.inspect : v.to_s}
352
+ (at 0 0 0)
353
+ (effects(font(size 1.27 1.27)) (hide yes))
354
+ )
355
+ })&.value
356
+ @children.append(prop) if prop
357
+ end
358
+ prop
359
+ end
360
+ end
361
+
362
+ class Extends < Node
363
+ value_types :library_id => String
364
+ end
365
+
366
+ class PinNumbers < Node
367
+ value_types :hide => [:no, :yes]
368
+ end
369
+
370
+ class PinNames < Node
371
+ child_types :offset
372
+ value_types :hide => [:no, :yes]
373
+ end
374
+
375
+ class InBom < Node
376
+ value_types :in_bom => [:no, :yes]
377
+ end
378
+
379
+ class OnBoard < Node
380
+ value_types :on_board => [:no, :yes]
381
+ end
382
+
383
+ class UnitName < Node
384
+ value_types :name => String
385
+ end
386
+
387
+ class EmbeddedFonts < Node
388
+ value_types :embedded_fonts => [:no, :yes]
389
+ end
390
+
391
+ class ExcludeFromSim < Node
392
+ value_types :exclude_from_sim => [:no, :yes]
142
393
  end
143
- =end
144
394
 
145
395
  end
146
396
  end
data/lib/kicad/grammar.rb CHANGED
@@ -33,19 +33,23 @@ module KiCad
33
33
 
34
34
  module Node2
35
35
  def s1
36
- elements[1]
36
+ elements[0]
37
37
  end
38
38
 
39
- def values
39
+ def s2
40
40
  elements[2]
41
41
  end
42
42
 
43
- def nodes
43
+ def values
44
44
  elements[3]
45
45
  end
46
46
 
47
- def s2
48
- elements[5]
47
+ def nodes
48
+ elements[4]
49
+ end
50
+
51
+ def s3
52
+ elements[6]
49
53
  end
50
54
  end
51
55
 
@@ -74,84 +78,88 @@ module KiCad
74
78
  end
75
79
 
76
80
  i0, s0 = index, []
77
- if (match_len = has_terminal?('(', false, index))
78
- r1 = true
79
- @index += match_len
80
- else
81
- terminal_parse_failure('\'(\'')
82
- r1 = nil
83
- end
81
+ r1 = _nt_s
84
82
  s0 << r1
85
83
  if r1
86
- r2 = _nt_s
84
+ if (match_len = has_terminal?('(', false, index))
85
+ r2 = true
86
+ @index += match_len
87
+ else
88
+ terminal_parse_failure('\'(\'')
89
+ r2 = nil
90
+ end
87
91
  s0 << r2
88
92
  if r2
89
- s3, i3 = [], index
90
- loop do
91
- i4, s4 = index, []
92
- r5 = _nt_value
93
- s4 << r5
94
- if r5
95
- r6 = _nt_s
96
- s4 << r6
97
- end
98
- if s4.last
99
- r4 = instantiate_node(SyntaxNode,input, i4...index, s4)
100
- r4.extend(Node0)
101
- else
102
- @index = i4
103
- r4 = nil
104
- end
105
- if r4
106
- s3 << r4
107
- else
108
- break
109
- end
110
- end
111
- if s3.empty?
112
- @index = i3
113
- r3 = nil
114
- else
115
- r3 = instantiate_node(SyntaxNode,input, i3...index, s3)
116
- end
93
+ r3 = _nt_s
117
94
  s0 << r3
118
95
  if r3
119
- s7, i7 = [], index
96
+ s4, i4 = [], index
120
97
  loop do
121
- i8, s8 = index, []
122
- r9 = _nt_node
123
- s8 << r9
124
- if r9
125
- r10 = _nt_s
126
- s8 << r10
98
+ i5, s5 = index, []
99
+ r6 = _nt_value
100
+ s5 << r6
101
+ if r6
102
+ r7 = _nt_s
103
+ s5 << r7
127
104
  end
128
- if s8.last
129
- r8 = instantiate_node(SyntaxNode,input, i8...index, s8)
130
- r8.extend(Node1)
105
+ if s5.last
106
+ r5 = instantiate_node(SyntaxNode,input, i5...index, s5)
107
+ r5.extend(Node0)
131
108
  else
132
- @index = i8
133
- r8 = nil
109
+ @index = i5
110
+ r5 = nil
134
111
  end
135
- if r8
136
- s7 << r8
112
+ if r5
113
+ s4 << r5
137
114
  else
138
115
  break
139
116
  end
140
117
  end
141
- r7 = instantiate_node(SyntaxNode,input, i7...index, s7)
142
- s0 << r7
143
- if r7
144
- if (match_len = has_terminal?(')', false, index))
145
- r11 = true
146
- @index += match_len
147
- else
148
- terminal_parse_failure('\')\'')
149
- r11 = nil
118
+ if s4.empty?
119
+ @index = i4
120
+ r4 = nil
121
+ else
122
+ r4 = instantiate_node(SyntaxNode,input, i4...index, s4)
123
+ end
124
+ s0 << r4
125
+ if r4
126
+ s8, i8 = [], index
127
+ loop do
128
+ i9, s9 = index, []
129
+ r10 = _nt_node
130
+ s9 << r10
131
+ if r10
132
+ r11 = _nt_s
133
+ s9 << r11
134
+ end
135
+ if s9.last
136
+ r9 = instantiate_node(SyntaxNode,input, i9...index, s9)
137
+ r9.extend(Node1)
138
+ else
139
+ @index = i9
140
+ r9 = nil
141
+ end
142
+ if r9
143
+ s8 << r9
144
+ else
145
+ break
146
+ end
150
147
  end
151
- s0 << r11
152
- if r11
153
- r12 = _nt_s
148
+ r8 = instantiate_node(SyntaxNode,input, i8...index, s8)
149
+ s0 << r8
150
+ if r8
151
+ if (match_len = has_terminal?(')', false, index))
152
+ r12 = true
153
+ @index += match_len
154
+ else
155
+ terminal_parse_failure('\')\'')
156
+ r12 = nil
157
+ end
154
158
  s0 << r12
159
+ if r12
160
+ r13 = _nt_s
161
+ s0 << r13
162
+ end
155
163
  end
156
164
  end
157
165
  end
data/lib/kicad/grammar.tt CHANGED
@@ -3,7 +3,7 @@ require 'kicad/ast'
3
3
  module KiCad
4
4
  grammar SExpr
5
5
  rule node
6
- '(' s values:( value s)+ nodes:(node s)* ')' s
6
+ s '(' s values:( value s)+ nodes:(node s)* ')' s
7
7
  { def value
8
8
  klass_name = values.elements[0].value.value
9
9
  klass = KiCad::AST::Node
data/lib/kicad/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module KiCad
2
- VERSION = "0.8.1"
2
+ VERSION = "0.9.4"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kicad
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clifford Heath
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2025-05-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -114,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
114
  - !ruby/object:Gem::Version
115
115
  version: '0'
116
116
  requirements: []
117
- rubygems_version: 3.6.8
117
+ rubygems_version: 3.6.2
118
118
  specification_version: 4
119
119
  summary: Load and rewrite Kicad s-expression files into a tree structure for scripting
120
120
  test_files: []