fritzkit 0.0.0-stage → 0.1.0

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 (328) hide show
  1. package/.claude-plugin/marketplace.json +12 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/CHANGELOG.md +8 -0
  4. package/LICENSE +21 -0
  5. package/README.md +429 -2
  6. package/dist/cli/commands/doctor.d.ts +4 -0
  7. package/dist/cli/commands/doctor.js +7 -0
  8. package/dist/cli/commands/doctor.js.map +1 -0
  9. package/dist/cli/commands/export.d.ts +17 -0
  10. package/dist/cli/commands/export.js +81 -0
  11. package/dist/cli/commands/export.js.map +1 -0
  12. package/dist/cli/commands/kicad.d.ts +10 -0
  13. package/dist/cli/commands/kicad.js +22 -0
  14. package/dist/cli/commands/kicad.js.map +1 -0
  15. package/dist/cli/commands/parts.d.ts +22 -0
  16. package/dist/cli/commands/parts.js +46 -0
  17. package/dist/cli/commands/parts.js.map +1 -0
  18. package/dist/cli/commands/prune.d.ts +14 -0
  19. package/dist/cli/commands/prune.js +67 -0
  20. package/dist/cli/commands/prune.js.map +1 -0
  21. package/dist/cli/commands/render.d.ts +24 -0
  22. package/dist/cli/commands/render.js +62 -0
  23. package/dist/cli/commands/render.js.map +1 -0
  24. package/dist/cli/commands/schema.d.ts +10 -0
  25. package/dist/cli/commands/schema.js +35 -0
  26. package/dist/cli/commands/schema.js.map +1 -0
  27. package/dist/cli/commands/ui.d.ts +14 -0
  28. package/dist/cli/commands/ui.js +67 -0
  29. package/dist/cli/commands/ui.js.map +1 -0
  30. package/dist/cli/commands/validate.d.ts +24 -0
  31. package/dist/cli/commands/validate.js +91 -0
  32. package/dist/cli/commands/validate.js.map +1 -0
  33. package/dist/cli/context.d.ts +81 -0
  34. package/dist/cli/context.js +139 -0
  35. package/dist/cli/context.js.map +1 -0
  36. package/dist/cli/main.d.ts +2 -0
  37. package/dist/cli/main.js +8 -0
  38. package/dist/cli/main.js.map +1 -0
  39. package/dist/cli/program.d.ts +21 -0
  40. package/dist/cli/program.js +158 -0
  41. package/dist/cli/program.js.map +1 -0
  42. package/dist/cli/raster.d.ts +4 -0
  43. package/dist/cli/raster.js +9 -0
  44. package/dist/cli/raster.js.map +1 -0
  45. package/dist/core/compile/breadboard-util.d.ts +12 -0
  46. package/dist/core/compile/breadboard-util.js +107 -0
  47. package/dist/core/compile/breadboard-util.js.map +1 -0
  48. package/dist/core/compile/breadboard.d.ts +60 -0
  49. package/dist/core/compile/breadboard.js +513 -0
  50. package/dist/core/compile/breadboard.js.map +1 -0
  51. package/dist/core/compile/fzp.d.ts +12 -0
  52. package/dist/core/compile/fzp.js +183 -0
  53. package/dist/core/compile/fzp.js.map +1 -0
  54. package/dist/core/compile/icon.d.ts +17 -0
  55. package/dist/core/compile/icon.js +27 -0
  56. package/dist/core/compile/icon.js.map +1 -0
  57. package/dist/core/compile/index.d.ts +4 -0
  58. package/dist/core/compile/index.js +62 -0
  59. package/dist/core/compile/index.js.map +1 -0
  60. package/dist/core/compile/pcb.d.ts +39 -0
  61. package/dist/core/compile/pcb.js +164 -0
  62. package/dist/core/compile/pcb.js.map +1 -0
  63. package/dist/core/compile/schematic.d.ts +69 -0
  64. package/dist/core/compile/schematic.js +240 -0
  65. package/dist/core/compile/schematic.js.map +1 -0
  66. package/dist/core/compile/style.d.ts +52 -0
  67. package/dist/core/compile/style.js +72 -0
  68. package/dist/core/compile/style.js.map +1 -0
  69. package/dist/core/compile/types.d.ts +50 -0
  70. package/dist/core/compile/types.js +15 -0
  71. package/dist/core/compile/types.js.map +1 -0
  72. package/dist/core/export/bin.d.ts +61 -0
  73. package/dist/core/export/bin.js +221 -0
  74. package/dist/core/export/bin.js.map +1 -0
  75. package/dist/core/export/fzpz.d.ts +37 -0
  76. package/dist/core/export/fzpz.js +121 -0
  77. package/dist/core/export/fzpz.js.map +1 -0
  78. package/dist/core/export/index.d.ts +5 -0
  79. package/dist/core/export/index.js +6 -0
  80. package/dist/core/export/index.js.map +1 -0
  81. package/dist/core/export/install.d.ts +77 -0
  82. package/dist/core/export/install.js +183 -0
  83. package/dist/core/export/install.js.map +1 -0
  84. package/dist/core/export/prune.d.ts +72 -0
  85. package/dist/core/export/prune.js +165 -0
  86. package/dist/core/export/prune.js.map +1 -0
  87. package/dist/core/fritzing.d.ts +22 -0
  88. package/dist/core/fritzing.js +77 -0
  89. package/dist/core/fritzing.js.map +1 -0
  90. package/dist/core/identity.d.ts +29 -0
  91. package/dist/core/identity.js +70 -0
  92. package/dist/core/identity.js.map +1 -0
  93. package/dist/core/image/bbox.d.ts +27 -0
  94. package/dist/core/image/bbox.js +24 -0
  95. package/dist/core/image/bbox.js.map +1 -0
  96. package/dist/core/image/index.d.ts +8 -0
  97. package/dist/core/image/index.js +9 -0
  98. package/dist/core/image/index.js.map +1 -0
  99. package/dist/core/image/key.d.ts +72 -0
  100. package/dist/core/image/key.js +317 -0
  101. package/dist/core/image/key.js.map +1 -0
  102. package/dist/core/image/prepare.d.ts +31 -0
  103. package/dist/core/image/prepare.js +98 -0
  104. package/dist/core/image/prepare.js.map +1 -0
  105. package/dist/core/image/remover.d.ts +17 -0
  106. package/dist/core/image/remover.js +51 -0
  107. package/dist/core/image/remover.js.map +1 -0
  108. package/dist/core/index.d.ts +16 -0
  109. package/dist/core/index.js +17 -0
  110. package/dist/core/index.js.map +1 -0
  111. package/dist/core/kicad/footprint.d.ts +104 -0
  112. package/dist/core/kicad/footprint.js +317 -0
  113. package/dist/core/kicad/footprint.js.map +1 -0
  114. package/dist/core/kicad/index.d.ts +25 -0
  115. package/dist/core/kicad/index.js +197 -0
  116. package/dist/core/kicad/index.js.map +1 -0
  117. package/dist/core/kicad/sexpr.d.ts +35 -0
  118. package/dist/core/kicad/sexpr.js +163 -0
  119. package/dist/core/kicad/sexpr.js.map +1 -0
  120. package/dist/core/resolve.d.ts +113 -0
  121. package/dist/core/resolve.js +301 -0
  122. package/dist/core/resolve.js.map +1 -0
  123. package/dist/core/schema/part.d.ts +995 -0
  124. package/dist/core/schema/part.js +278 -0
  125. package/dist/core/schema/part.js.map +1 -0
  126. package/dist/core/schema/part.schema.json +1122 -0
  127. package/dist/core/svg.d.ts +20 -0
  128. package/dist/core/svg.js +56 -0
  129. package/dist/core/svg.js.map +1 -0
  130. package/dist/core/units.d.ts +28 -0
  131. package/dist/core/units.js +42 -0
  132. package/dist/core/units.js.map +1 -0
  133. package/dist/core/validate/checker.d.ts +46 -0
  134. package/dist/core/validate/checker.js +142 -0
  135. package/dist/core/validate/checker.js.map +1 -0
  136. package/dist/core/validate/doctor.d.ts +25 -0
  137. package/dist/core/validate/doctor.js +156 -0
  138. package/dist/core/validate/doctor.js.map +1 -0
  139. package/dist/core/validate/explain.d.ts +10 -0
  140. package/dist/core/validate/explain.js +135 -0
  141. package/dist/core/validate/explain.js.map +1 -0
  142. package/dist/core/validate/index.d.ts +21 -0
  143. package/dist/core/validate/index.js +96 -0
  144. package/dist/core/validate/index.js.map +1 -0
  145. package/dist/core/validate/structural.d.ts +74 -0
  146. package/dist/core/validate/structural.js +664 -0
  147. package/dist/core/validate/structural.js.map +1 -0
  148. package/dist/core/workspace.d.ts +69 -0
  149. package/dist/core/workspace.js +213 -0
  150. package/dist/core/workspace.js.map +1 -0
  151. package/dist/mcp/jsonpatch.d.ts +30 -0
  152. package/dist/mcp/jsonpatch.js +145 -0
  153. package/dist/mcp/jsonpatch.js.map +1 -0
  154. package/dist/mcp/main.d.ts +2 -0
  155. package/dist/mcp/main.js +5 -0
  156. package/dist/mcp/main.js.map +1 -0
  157. package/dist/mcp/server.d.ts +28 -0
  158. package/dist/mcp/server.js +310 -0
  159. package/dist/mcp/server.js.map +1 -0
  160. package/dist/server/api.d.ts +72 -0
  161. package/dist/server/api.js +507 -0
  162. package/dist/server/api.js.map +1 -0
  163. package/dist/server/events.d.ts +28 -0
  164. package/dist/server/events.js +134 -0
  165. package/dist/server/events.js.map +1 -0
  166. package/dist/server/http.d.ts +36 -0
  167. package/dist/server/http.js +127 -0
  168. package/dist/server/http.js.map +1 -0
  169. package/dist/server/index.d.ts +65 -0
  170. package/dist/server/index.js +193 -0
  171. package/dist/server/index.js.map +1 -0
  172. package/dist/ui/assets/index-DLU8opyI.js +63 -0
  173. package/dist/ui/assets/index-GhUA-T3b.css +1 -0
  174. package/dist/ui/index.html +14 -0
  175. package/package.json +89 -4
  176. package/skills/fritzkit/SKILL.md +690 -0
  177. package/skills/fritzkit/reference/examples.md +275 -0
  178. package/skills/fritzkit/reference/part-json.md +333 -0
  179. package/skills/fritzkit/scripts/measure.py +197 -0
  180. package/tools/fzp-checker/.python-version +1 -0
  181. package/tools/fzp-checker/README.md +45 -0
  182. package/tools/fzp-checker/check_fzp.py +48 -0
  183. package/tools/fzp-checker/pyproject.toml +26 -0
  184. package/tools/fzp-checker/uv.lock +266 -0
  185. package/tools/fzp-checker/vendor/LICENSE.txt +8 -0
  186. package/tools/fzp-checker/vendor/UPSTREAM.md +25 -0
  187. package/tools/fzp-checker/vendor/fzp_checker.py +14 -0
  188. package/tools/fzp-checker/vendor/scripts/checks/__init__.py +26 -0
  189. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/__init__.cpython-313.pyc +0 -0
  190. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/check_missing_leg_ids.cpython-313.pyc +0 -0
  191. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/connector_numbering.cpython-313.pyc +0 -0
  192. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_checker_runner.cpython-313.pyc +0 -0
  193. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_checkers.cpython-313.pyc +0 -0
  194. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_svg_checkers.cpython-313.pyc +0 -0
  195. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_utils.cpython-313.pyc +0 -0
  196. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/svg_checkers.cpython-313.pyc +0 -0
  197. package/tools/fzp-checker/vendor/scripts/checks/__pycache__/svg_utils.cpython-313.pyc +0 -0
  198. package/tools/fzp-checker/vendor/scripts/checks/check_missing_leg_ids.py +44 -0
  199. package/tools/fzp-checker/vendor/scripts/checks/checkascii.py +27 -0
  200. package/tools/fzp-checker/vendor/scripts/checks/checkcase.py +156 -0
  201. package/tools/fzp-checker/vendor/scripts/checks/checkcopies.py +104 -0
  202. package/tools/fzp-checker/vendor/scripts/checks/connector_numbering.py +79 -0
  203. package/tools/fzp-checker/vendor/scripts/checks/connectors_misnumbered.py +228 -0
  204. package/tools/fzp-checker/vendor/scripts/checks/explain_errors.md +95 -0
  205. package/tools/fzp-checker/vendor/scripts/checks/fzp_checker_runner.py +717 -0
  206. package/tools/fzp-checker/vendor/scripts/checks/fzp_checkers.py +1069 -0
  207. package/tools/fzp-checker/vendor/scripts/checks/fzp_svg_checkers.py +62 -0
  208. package/tools/fzp-checker/vendor/scripts/checks/fzp_utils.py +222 -0
  209. package/tools/fzp-checker/vendor/scripts/checks/inspector_fc51.png +0 -0
  210. package/tools/fzp-checker/vendor/scripts/checks/invisibleconnectors.py +118 -0
  211. package/tools/fzp-checker/vendor/scripts/checks/nolayeridcheck.txt +758 -0
  212. package/tools/fzp-checker/vendor/scripts/checks/requirements.txt +3 -0
  213. package/tools/fzp-checker/vendor/scripts/checks/svg_checkers.py +953 -0
  214. package/tools/fzp-checker/vendor/scripts/checks/svg_utils.py +96 -0
  215. package/tools/fzp-checker/vendor/scripts/checks/test_checkers.py +839 -0
  216. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/author_missing.fzp.test +3 -0
  217. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/author_present.fzp.test +4 -0
  218. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/buses_missing_id.fzp.test +8 -0
  219. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/buses_missing_node_members.fzp.test +7 -0
  220. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/buses_present_valid.fzp.test +8 -0
  221. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_layers_missing_attributes.fzp.test +16 -0
  222. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_layers_present_valid.fzp.test +14 -0
  223. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_numbering_invalid.fzp.test +70 -0
  224. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_numbering_valid.fzp.test +70 -0
  225. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_refs_valid.fzp.test +53 -0
  226. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_mixed.fzp.test +11 -0
  227. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_silkscreen_nested.fzp.test +11 -0
  228. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_style_attr.fzp.test +11 -0
  229. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_white_stroke.fzp.test +11 -0
  230. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_wrong_color.fzp.test +11 -0
  231. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_inherited.fzp.test +11 -0
  232. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_legacy.fzp.test +12 -0
  233. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_nested_copper.fzp.test +12 -0
  234. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_none.fzp.test +11 -0
  235. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_standard.fzp.test +12 -0
  236. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_uppercase.fzp.test +11 -0
  237. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/css_connector.fzp.test +100 -0
  238. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/description_missing.fzp.test +3 -0
  239. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/description_present.fzp.test +4 -0
  240. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/duplicate_ids.fzp.test +27 -0
  241. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/family_property_missing.fzp.test +6 -0
  242. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/family_property_present_valid.fzp.test +6 -0
  243. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/font_size.fzp.test +46 -0
  244. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/fritzing_version_invalid_format.fzp.test +4 -0
  245. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/fritzing_version_missing.fzp.test +4 -0
  246. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/fritzing_version_present_valid.fzp.test +4 -0
  247. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/gorn_absent.fzp.test +15 -0
  248. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/gorn_present.fzp.test +15 -0
  249. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/hybrid_connectors.fzp.test +232 -0
  250. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/invalid_xml.fzp.test +12 -0
  251. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_ids_match.fzp.test +29 -0
  252. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_ids_mismatch.fzp.test +30 -0
  253. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_nesting_invalid.fzp.test +11 -0
  254. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_nesting_valid.fzp.test +11 -0
  255. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/matrix_transform.fzp.test +8 -0
  256. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/missing_connector_refs.fzp.test +63 -0
  257. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/missing_leg_ids.fzp.test +34 -0
  258. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_missing.fzp.test +4 -0
  259. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_non_alnum.fzp.test +4 -0
  260. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_present.fzp.test +4 -0
  261. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_special_chars_absent.fzp.test +4 -0
  262. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_special_chars_present.fzp.test +4 -0
  263. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_too_short.fzp.test +4 -0
  264. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/no_layer_invalid.fzp.test +19 -0
  265. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/no_layer_namedview.fzp.test +19 -0
  266. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/no_layer_valid.fzp.test +19 -0
  267. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/pcb_only.fzp.test +48 -0
  268. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/property_fields_missing_name.fzp.test +6 -0
  269. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/property_fields_missing_value.fzp.test +6 -0
  270. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/property_fields_present.fzp.test +6 -0
  271. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/required_tags_missing.fzp.test +3 -0
  272. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/required_tags_present.fzp.test +11 -0
  273. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/stroke_test.fzp.test +60 -0
  274. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/template_svgs.fzp.test +35 -0
  275. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/title_missing.fzp.test +3 -0
  276. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/title_present.fzp.test +4 -0
  277. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/unique_ids.fzp.test +27 -0
  278. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/unique_property_names_duplicates.fzp.test +7 -0
  279. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/unique_property_names_unique.fzp.test +7 -0
  280. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/valid_xml.fzp.test +249 -0
  281. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/version_invalid_format.fzp.test +4 -0
  282. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/version_missing.fzp.test +3 -0
  283. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/version_present_valid.fzp.test +4 -0
  284. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/views_missing.fzp.test +3 -0
  285. package/tools/fzp-checker/vendor/scripts/checks/test_data/core/views_present.fzp.test +8 -0
  286. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/connector_refs_valid_breadboard.svg +7 -0
  287. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/duplicate_ids_breadboard.svg +13 -0
  288. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/gorn_absent_breadboard.svg +7 -0
  289. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/gorn_present_breadboard.svg +7 -0
  290. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/hybrid_connectors_breadboard.svg +102 -0
  291. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/layer_nesting_invalid.svg +13 -0
  292. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/layer_nesting_valid.svg +13 -0
  293. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/matrix_transform.svg +11 -0
  294. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/missing_connector_refs_breadboard.svg +8 -0
  295. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/missing_leg_ids_breadboard.svg +9 -0
  296. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/test_layer_ids_match_breadboard.svg +9 -0
  297. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/test_layer_ids_mismatch_breadboard.svg +9 -0
  298. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/unique_ids_breadboard.svg +13 -0
  299. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/hybrid_connectors_icon.svg +78 -0
  300. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/no_layer_invalid.svg +4 -0
  301. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/no_layer_namedview.svg +9 -0
  302. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/no_layer_valid.svg +7 -0
  303. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/plain_pcb.svg +10 -0
  304. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/connector_refs_valid_pcb.svg +10 -0
  305. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_mixed.svg +8 -0
  306. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_silkscreen_nested.svg +10 -0
  307. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_style_attr.svg +6 -0
  308. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_white_stroke.svg +6 -0
  309. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_wrong_color.svg +6 -0
  310. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_inherited.svg +7 -0
  311. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_legacy.svg +10 -0
  312. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_nested_copper.svg +9 -0
  313. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_none.svg +7 -0
  314. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_standard.svg +10 -0
  315. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_uppercase.svg +7 -0
  316. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/css_connector.svg +25 -0
  317. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/font_size.svg +54 -0
  318. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/hybrid_connectors_pcb.svg +35 -0
  319. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/missing_connector_refs_pcb.svg +14 -0
  320. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/pcb-arduino-shield.svg +25 -0
  321. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/stroke_test.svg +12 -0
  322. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/test_layer_ids_match_pcb.svg +12 -0
  323. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/test_layer_ids_mismatch_pcb.svg +12 -0
  324. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/connector_refs_valid_schematic.svg +7 -0
  325. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/hybrid_connectors_schematic.svg +59 -0
  326. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/missing_connector_refs_schematic.svg +8 -0
  327. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/test_layer_ids_match_schematic.svg +6 -0
  328. package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/test_layer_ids_mismatch_schematic.svg +6 -0
@@ -0,0 +1,1069 @@
1
+ import os
2
+ from lxml import etree
3
+ from abc import ABC, abstractmethod
4
+ from .fzp_utils import FZPUtils
5
+ from .svg_utils import SVGUtils
6
+ import re
7
+ import logging
8
+ import datetime
9
+
10
+
11
+ class ValidationIssue:
12
+ """Represents a validation issue with node reference"""
13
+
14
+ def __init__(self, message, severity='error', node=None):
15
+ self.message = message
16
+ self.severity = severity
17
+ self.node = node
18
+
19
+ class FixResult:
20
+ """Represents a fix that was applied during checking"""
21
+
22
+ def __init__(self, message, node=None, line_number=None):
23
+ self.message = message
24
+ self.node = node
25
+ self.line_number = line_number
26
+
27
+ class FZPChecker(ABC):
28
+ def __init__(self, fzp_doc):
29
+ self.fzp_doc = fzp_doc
30
+ self.issues = []
31
+ self.fixes = []
32
+ self.logger = logging.getLogger(self.__class__.__name__)
33
+
34
+ @abstractmethod
35
+ def check(self):
36
+ pass
37
+
38
+ def add_error(self, message, node=None):
39
+ issue = ValidationIssue(message, severity='error', node=node)
40
+ self.issues.append(issue)
41
+ self.logger.debug(f"Error: {message}")
42
+
43
+ def add_warning(self, message, node=None):
44
+ issue = ValidationIssue(message, severity='warning', node=node)
45
+ self.issues.append(issue)
46
+ self.logger.debug(f"Warning: {message}")
47
+
48
+ def add_fix(self, message, node=None, line_number=None):
49
+ fix = FixResult(message, node=node, line_number=line_number)
50
+ self.fixes.append(fix)
51
+ self.logger.debug(f"Fixed: {message}")
52
+
53
+ def get_result(self):
54
+ errors = len([i for i in self.issues if i.severity == 'error'])
55
+ warnings = len([i for i in self.issues if i.severity == 'warning'])
56
+ return errors, warnings
57
+
58
+ def get_fixes_count(self):
59
+ return len(self.fixes)
60
+
61
+ @staticmethod
62
+ @abstractmethod
63
+ def get_name():
64
+ pass
65
+
66
+ @staticmethod
67
+ @abstractmethod
68
+ def get_description():
69
+ pass
70
+
71
+
72
+ class FZPMissingTagsChecker(FZPChecker):
73
+ def check(self):
74
+ required_tags = ["module", "version", "author", "title", "label", "date", "description", "views", "connectors"]
75
+ root = self.fzp_doc.getroot()
76
+ for tag in required_tags:
77
+ elements = self.fzp_doc.xpath(f"//{tag}")
78
+ if not elements:
79
+ self.add_error(f"Missing required tag: {tag}", node=root)
80
+ return self.get_result()
81
+
82
+ @staticmethod
83
+ def get_name():
84
+ return "missing_tags"
85
+
86
+ @staticmethod
87
+ def get_description():
88
+ return "Check for missing required tags in the FZP file"
89
+
90
+
91
+ class FZPConnectorTerminalChecker(FZPChecker):
92
+ def __init__(self, fzp_doc, svg_docs):
93
+ super().__init__(fzp_doc)
94
+ self.svg_docs = svg_docs
95
+
96
+ def _find_invalid_terminal_ids(self):
97
+ """
98
+ Private helper method to find invalid terminal IDs in connectors.
99
+
100
+ Yields:
101
+ Tuple containing:
102
+ - connector Element
103
+ - p Element with missing terminalId
104
+ - terminal_id string
105
+ """
106
+ connectors_section = self.fzp_doc.xpath("//module/connectors")
107
+ if not connectors_section:
108
+ print("No connectors section found in the FZP file.")
109
+ return
110
+
111
+ connectors = connectors_section[0].xpath("connector")
112
+ for connector in connectors:
113
+ connector_id = connector.attrib.get("id", "unknown")
114
+ views = connector.xpath("views")
115
+ if not views:
116
+ continue
117
+ views = views[0]
118
+ for view in views:
119
+ if view.tag != "schematicView":
120
+ continue
121
+
122
+ p_elements = view.xpath("p[@terminalId]")
123
+ for p in p_elements:
124
+ terminal_id = p.attrib.get("terminalId")
125
+ if not self.svg_has_element_with_id(terminal_id, view.tag):
126
+ yield (connector, p, terminal_id, connector_id)
127
+
128
+ def check(self):
129
+ for connector, p_element, terminal_id, connector_id in self._find_invalid_terminal_ids():
130
+ self.add_error(f"Connector '{connector_id}' references missing terminal '{terminal_id}' in SVG", node=p_element)
131
+ return self.get_result()
132
+
133
+ def svg_has_element_with_id(self, element_id, view_name):
134
+ svg_doc = self.svg_docs.get(view_name)
135
+ if not svg_doc:
136
+ return True # Skip if SVG not available
137
+ try:
138
+ elements = svg_doc.xpath(f"//*[@id='{element_id}']")
139
+ return len(elements) > 0
140
+ except Exception as e:
141
+ print(f"Error processing {view_name} SVG: {str(e)}")
142
+ return True # Not a 'missing element' if there's an error
143
+ return False
144
+
145
+ def fix(self, filename):
146
+ """
147
+ Removes invalid terminalId attributes from the FZP XML.
148
+
149
+ Args:
150
+ filename: Path to the FZP file to write fixes to
151
+
152
+ Returns:
153
+ bool: True if modifications were made and saved successfully, False otherwise.
154
+ """
155
+ modified = False
156
+
157
+ for connector, p_element, terminal_id, connector_id in self._find_invalid_terminal_ids():
158
+ # Remove the terminalId attribute
159
+ del p_element.attrib["terminalId"]
160
+ print(f"Removed missing terminalId '{terminal_id}' from connector '{connector_id}' in schematicView.")
161
+ modified = True
162
+
163
+ if modified:
164
+ # Create a backup before modifying
165
+ backup_path = filename + ".bak"
166
+ if not os.path.exists(backup_path):
167
+ self.fzp_doc.write(backup_path, pretty_print=True, xml_declaration=True, encoding='UTF-8')
168
+ print(f"Backup created at '{backup_path}'.")
169
+
170
+ # Write the modified XML back to the FZP file
171
+ self.fzp_doc.write(filename, pretty_print=True, xml_declaration=True, encoding='UTF-8')
172
+ print(f"FZP file '{filename}' has been updated successfully.")
173
+ return True
174
+ else:
175
+ print("No invalid terminal IDs found. No changes made.")
176
+ return False
177
+
178
+ @staticmethod
179
+ def get_name():
180
+ return "connector_terminal"
181
+
182
+ @staticmethod
183
+ def get_description():
184
+ return "Check if the connector terminals defined in the FZP file exist in the referenced SVGs"
185
+
186
+
187
+ class FZPConnectorSvgRefChecker(FZPChecker):
188
+ def __init__(self, fzp_doc, svg_docs):
189
+ super().__init__(fzp_doc)
190
+ self.svg_docs = svg_docs
191
+
192
+ def check(self):
193
+ connectors_section = self.fzp_doc.xpath("//module/connectors")
194
+ if connectors_section:
195
+ connectors = connectors_section[0].xpath("connector")
196
+ for connector in connectors:
197
+ connector_id = connector.attrib["id"]
198
+ views = connector.xpath("views")[0]
199
+ for view in views:
200
+ p_elements = view.xpath("p")
201
+ for p in p_elements:
202
+ # Skip legs
203
+ if 'legId' in p.attrib:
204
+ continue
205
+ # Skip hybrids or unknown layers
206
+ if FZPUtils.is_hybrid_or_unknown_layer(p):
207
+ continue
208
+
209
+ connector_svg_id = p.attrib.get("svgId")
210
+ layer = p.attrib.get("layer")
211
+ if not connector_svg_id:
212
+ self.add_error(f"Connector {connector_id} does not reference an element in layer {layer}.")
213
+ return self.get_result()
214
+
215
+ @staticmethod
216
+ def get_name():
217
+ return "connector_svg_ref"
218
+
219
+ @staticmethod
220
+ def get_description():
221
+ return "Check if connectors reference SVG elements via svgId attribute"
222
+
223
+
224
+ class FZPPCBConnectorStrokeChecker(FZPChecker):
225
+ def __init__(self, fzp_doc, svg_docs):
226
+ super().__init__(fzp_doc)
227
+ self.svg_docs = svg_docs
228
+
229
+ def check(self):
230
+ connectors_section = self.fzp_doc.xpath("//module/connectors")
231
+ if connectors_section:
232
+ connectors = connectors_section[0].xpath("connector")
233
+ for connector in connectors:
234
+ connector_id = connector.attrib["id"]
235
+ views = connector.xpath("views")[0]
236
+ for view in views:
237
+ if view.tag != "pcbView":
238
+ continue
239
+
240
+ p_elements = view.xpath("p")
241
+ for p in p_elements:
242
+ connector_svg_id = p.attrib.get("svgId")
243
+ if not connector_svg_id:
244
+ continue
245
+
246
+ if not self.is_connector_stroke_valid(view.tag, connector_svg_id):
247
+ self.add_error(f"Invalid stroke for connector '{connector_svg_id}' in PCB view")
248
+ return self.get_result()
249
+
250
+ def is_connector_stroke_valid(self, view_name, connector_id):
251
+ svg_doc = self.svg_docs.get(view_name)
252
+ if not svg_doc:
253
+ return True
254
+
255
+ try:
256
+ elements = svg_doc.xpath(f"//*[@id='{connector_id}']")
257
+ if elements:
258
+ try:
259
+ return SVGUtils.has_valid_stroke(elements[0])
260
+ except ValueError as e:
261
+ self.add_error(f"Failure with {connector_id}: {e}")
262
+ return True # Connector not found, skip further checks
263
+ else:
264
+ self.add_error(f"Connector {connector_id} not found in {view_name} SVG")
265
+ return True
266
+ except Exception as e:
267
+ self.add_error(f"Error processing {view_name} SVG: {str(e)}")
268
+ return True
269
+ return False
270
+
271
+ @staticmethod
272
+ def get_name():
273
+ return "pcb_connector_stroke"
274
+
275
+ @staticmethod
276
+ def get_description():
277
+ return "Check for valid stroke attributes in connectors of the PCB view in the SVG files referenced by the FZP"
278
+
279
+ class FZPFritzingVersionChecker(FZPChecker):
280
+ def check(self):
281
+ root = self.fzp_doc.getroot()
282
+ version = root.get('fritzingVersion')
283
+ if not version:
284
+ self.add_error("'FritzingVersion' is undefined or empty.", node=root)
285
+ else:
286
+ # Requires a Fritzing release version as announced on the blog or download page.
287
+ version_pattern = r'^\d+\.\d+\.\d+.*$'
288
+ if not re.match(version_pattern, version.strip()):
289
+ self.add_error(f"'FritzingVersion' '{version}' should be in semantic versioning format (https://semver.org/).", node=root)
290
+ return self.get_result()
291
+
292
+ @staticmethod
293
+ def get_name():
294
+ return "fritzing_version"
295
+
296
+ @staticmethod
297
+ def get_description():
298
+ return "Check fritzing version attribute is present and valid"
299
+
300
+
301
+ class FZPFritzingVersionRangeChecker(FZPChecker):
302
+ """Check that fritzingVersion is not too old."""
303
+
304
+ @staticmethod
305
+ def _parse_version(version_str):
306
+ """Parse a version string into a comparable tuple of ints.
307
+
308
+ Handles formats like '0.9.3b', '0.5.2b.02.18.4756', '1.0.3'.
309
+ Only the first three numeric components are compared.
310
+ """
311
+ if not version_str:
312
+ return None
313
+ parts = version_str.strip().split('.')
314
+ result = []
315
+ for part in parts[:3]:
316
+ digits = re.match(r'(\d+)', part)
317
+ if digits:
318
+ result.append(int(digits.group(1)))
319
+ else:
320
+ break
321
+ if len(result) < 3:
322
+ result.extend([0] * (3 - len(result)))
323
+ return tuple(result[:3])
324
+
325
+ def check(self):
326
+ root = self.fzp_doc.getroot()
327
+ version_str = root.get('fritzingVersion')
328
+ if not version_str:
329
+ # Missing version is handled by FZPFritzingVersionChecker
330
+ return self.get_result()
331
+
332
+ version = self._parse_version(version_str)
333
+ if version is None:
334
+ return self.get_result()
335
+
336
+ if version < (0, 8, 0):
337
+ self.add_error(
338
+ f"fritzingVersion '{version_str}' is below 0.8. "
339
+ f"This part is very old and likely needs a full update.",
340
+ node=root
341
+ )
342
+ if version < (0, 9, 4):
343
+ self.add_warning(
344
+ f"fritzingVersion '{version_str}' — this part claims a fritzingVersion that is more than ten years old.",
345
+ node=root
346
+ )
347
+ elif version < (1, 0, 4):
348
+ self.add_warning(
349
+ f"fritzingVersion '{version_str}' — this part might use some outdated conventions.",
350
+ node=root
351
+ )
352
+ return self.get_result()
353
+
354
+ @staticmethod
355
+ def get_name():
356
+ return "fritzing_version_range"
357
+
358
+ @staticmethod
359
+ def get_description():
360
+ return "Check that fritzingVersion is not too old (error if < 0.8, warning if < 0.9.4 or < 1.0.4)"
361
+
362
+
363
+ class FZPModuleIDChecker(FZPChecker):
364
+ def check(self):
365
+ root = self.fzp_doc.getroot()
366
+ module_id = root.get('moduleId')
367
+ if not module_id:
368
+ self.add_error("'ModuleID' is undefined or empty.", node=root)
369
+ return self.get_result()
370
+
371
+ @staticmethod
372
+ def get_name():
373
+ return "module_id"
374
+
375
+ @staticmethod
376
+ def get_description():
377
+ return "Check module ID attribute is present"
378
+
379
+ class FZPModuleIDSpecialCharsChecker(FZPChecker):
380
+ # Characters that are unsafe for filenames on Windows, Linux, or macOS
381
+ FILENAME_UNSAFE_CHARS = set('<>:"/\\|?*\0')
382
+ # Control characters (0x00-0x1F)
383
+ CONTROL_CHARS = set(chr(c) for c in range(0, 32))
384
+ MIN_LENGTH = 8
385
+
386
+ def check(self):
387
+ root = self.fzp_doc.getroot()
388
+ module_id = root.get('moduleId')
389
+ if not module_id:
390
+ return self.get_result()
391
+
392
+ if len(module_id) < self.MIN_LENGTH:
393
+ self.add_error(f"ModuleID '{module_id}' is too short (minimum {self.MIN_LENGTH} characters)", node=root)
394
+
395
+ reported_errors = set()
396
+ reported_warnings = set()
397
+
398
+ for char in module_id:
399
+ if char in self.CONTROL_CHARS or char in self.FILENAME_UNSAFE_CHARS:
400
+ if char not in reported_errors:
401
+ reported_errors.add(char)
402
+ if ord(char) < 32:
403
+ self.add_error(f"ModuleID contains control character 0x{ord(char):02X} which is unsafe for filenames", node=root)
404
+ else:
405
+ self.add_error(f"ModuleID contains character '{char}' which is unsafe for filenames", node=root)
406
+ elif not (char.isalnum() or char in '-_.'):
407
+ if char not in reported_warnings:
408
+ reported_warnings.add(char)
409
+ if char == ' ':
410
+ self.add_warning("ModuleID contains spaces", node=root)
411
+ else:
412
+ self.add_warning(f"ModuleID contains non-alphanumeric character '{char}'", node=root)
413
+
414
+ return self.get_result()
415
+
416
+ def fix(self, filename):
417
+ """Replace unsafe characters in moduleId with underscores."""
418
+ with open(filename, 'r', encoding='UTF-8') as f:
419
+ content = f.read()
420
+
421
+ original_content = content
422
+
423
+ pattern = r'(moduleId\s*=\s*")([^"]*?)(")'
424
+
425
+ def replace_module_id(match):
426
+ prefix = match.group(1)
427
+ module_id = match.group(2)
428
+ suffix = match.group(3)
429
+
430
+ sanitized = []
431
+ for char in module_id:
432
+ if char.isalnum() or char in '-_.':
433
+ sanitized.append(char)
434
+ else:
435
+ sanitized.append('_')
436
+ sanitized = ''.join(sanitized)
437
+
438
+ if sanitized != module_id:
439
+ self.add_fix(f"Sanitized moduleId from '{module_id}' to '{sanitized}'")
440
+ return f"{prefix}{sanitized}{suffix}"
441
+ return match.group(0)
442
+
443
+ content = re.sub(pattern, replace_module_id, content)
444
+
445
+ if content != original_content:
446
+ with open(filename, 'w', encoding='UTF-8') as f:
447
+ f.write(content)
448
+
449
+ return self.fixes
450
+
451
+ @staticmethod
452
+ def get_name():
453
+ return "module_id_special_chars"
454
+
455
+ @staticmethod
456
+ def get_description():
457
+ return "Check module ID for characters that are unsafe for filenames or not alphanumeric"
458
+
459
+
460
+ class FZPVersionChecker(FZPChecker):
461
+ def check(self):
462
+ version_elements = self.fzp_doc.xpath("//version")
463
+ if not version_elements:
464
+ self.add_warning("'Version' is undefined.", node=self.fzp_doc.getroot())
465
+ else:
466
+ version_element = version_elements[0]
467
+ version = version_element.text
468
+ if not re.match(r'^\d+(\.\d+)*$', version):
469
+ self.add_warning(f"'Version' '{version}' does not match the expected format.", node=version_element)
470
+ return self.get_result()
471
+
472
+ @staticmethod
473
+ def get_name():
474
+ return "version"
475
+
476
+ @staticmethod
477
+ def get_description():
478
+ return "Check version tag is present and valid"
479
+
480
+
481
+ class FZPTitleChecker(FZPChecker):
482
+ def check(self):
483
+ title_elements = self.fzp_doc.xpath("//title")
484
+ if not title_elements:
485
+ self.add_error("'Title' is undefined or empty.", node=self.fzp_doc.getroot())
486
+ return self.get_result()
487
+
488
+ @staticmethod
489
+ def get_name():
490
+ return "title"
491
+
492
+ @staticmethod
493
+ def get_description():
494
+ return "Check title tag is present"
495
+
496
+
497
+ class FZPDescriptionChecker(FZPChecker):
498
+ def check(self):
499
+ description_elements = self.fzp_doc.xpath("//description")
500
+ if not description_elements:
501
+ self.add_warning("'Description' is undefined.", node=self.fzp_doc.getroot())
502
+ return self.get_result()
503
+
504
+ @staticmethod
505
+ def get_name():
506
+ return "description"
507
+
508
+ @staticmethod
509
+ def get_description():
510
+ return "Check description tag is present"
511
+
512
+
513
+ class FZPAuthorChecker(FZPChecker):
514
+ def check(self):
515
+ author_elements = self.fzp_doc.xpath("//author")
516
+ if not author_elements:
517
+ self.add_warning("'Author' is undefined.", node=self.fzp_doc.getroot())
518
+ return self.get_result()
519
+
520
+ @staticmethod
521
+ def get_name():
522
+ return "author"
523
+
524
+ @staticmethod
525
+ def get_description():
526
+ return "Check author tag is present"
527
+
528
+
529
+ class FZPViewsChecker(FZPChecker):
530
+ def check(self):
531
+ views = self.fzp_doc.xpath("//views")
532
+ if not views:
533
+ self.add_error("'views' section is missing.", node=self.fzp_doc.getroot())
534
+ return self.get_result()
535
+
536
+ views_element = views[0]
537
+ required_views = ['breadboardView', 'pcbView', 'schematicView']
538
+ for view in required_views:
539
+ if not views_element.xpath(f".//{view}"):
540
+ self.add_error(f"Required view '{view}' is missing.", node=views_element)
541
+ return self.get_result()
542
+
543
+ @staticmethod
544
+ def get_name():
545
+ return "views"
546
+
547
+ @staticmethod
548
+ def get_description():
549
+ return "Check views section is present"
550
+
551
+
552
+ class FZPBusIDChecker(FZPChecker):
553
+ def check(self):
554
+ buses = self.fzp_doc.xpath("//bus")
555
+ for bus in buses:
556
+ if not bus.get('id'):
557
+ self.add_error(f"Bus with missing ID found: {etree.tostring(bus, pretty_print=True).decode()}", node=bus)
558
+ return self.get_result()
559
+
560
+ @staticmethod
561
+ def get_name():
562
+ return "bus_id"
563
+
564
+ @staticmethod
565
+ def get_description():
566
+ return "Check bus IDs are present"
567
+
568
+
569
+ class FZPBusNodesChecker(FZPChecker):
570
+ def __init__(self, fzp_doc):
571
+ super().__init__(fzp_doc)
572
+ self.buses_with_no_nodes = []
573
+
574
+ def check(self):
575
+ buses = self.fzp_doc.xpath("//bus")
576
+ for bus in buses:
577
+ nodes = bus.xpath(".//nodeMember")
578
+ if not nodes:
579
+ bus_id = bus.get('id', 'unknown')
580
+ self.add_error(f"Bus '{bus_id}' has no node members.", node=bus)
581
+ self.buses_with_no_nodes.append(bus_id)
582
+ else:
583
+ for node in nodes:
584
+ if not node.get('connectorId'):
585
+ bus_id = bus.get('id', 'unknown')
586
+ self.add_error(f"Node missing connectorId in Bus '{bus_id}'.", node=node)
587
+ return self.get_result()
588
+
589
+ def fix(self, filename):
590
+ """Remove buses that have no node members by treating XML as string and removing the relevant blocks."""
591
+ if not self.buses_with_no_nodes:
592
+ return False # Nothing to fix
593
+
594
+ fixed = False
595
+
596
+ with open(filename, 'r', encoding='UTF-8') as f:
597
+ content = f.read()
598
+
599
+ for bus_id in self.buses_with_no_nodes:
600
+ # Pattern includes leading whitespace and entire line
601
+ pattern = re.compile(
602
+ r'[\t ]*<bus\b[^>]*\bid\s*=\s*["\']{}["\'][^>]*/?>(?:[^<]*</bus>)?\r?\n'.format(re.escape(bus_id))
603
+ )
604
+ new_content, count = pattern.subn('', content)
605
+ if count > 0:
606
+ fixed = True
607
+ print(f"Fixed: Removed empty bus '{bus_id}'")
608
+ content = new_content
609
+
610
+ if fixed:
611
+ with open(filename, 'w', encoding='UTF-8') as f:
612
+ f.write(content)
613
+
614
+ return fixed
615
+
616
+ @staticmethod
617
+ def get_name():
618
+ return "bus_nodes"
619
+
620
+ @staticmethod
621
+ def get_description():
622
+ return "Check bus nodes are present and valid"
623
+
624
+
625
+ class FZPConnectorLayersChecker(FZPChecker):
626
+ def check(self):
627
+ connectors = self.fzp_doc.xpath("//connector")
628
+ for connector in connectors:
629
+ connector_id = connector.get('id')
630
+ layers = connector.xpath(".//ConnectorLayer")
631
+ for layer in layers:
632
+ if not layer.get('layer'):
633
+ self.add_error(f"ConnectorLayer missing 'layer' ID in Connector '{connector_id}'.", node=layer)
634
+ if not layer.get('svgId'):
635
+ self.add_error(f"ConnectorLayer missing 'svgId' in Connector '{connector_id}'.", node=layer)
636
+ return self.get_result()
637
+
638
+ @staticmethod
639
+ def get_name():
640
+ return "connector_layers"
641
+
642
+ @staticmethod
643
+ def get_description():
644
+ return "Check connector layers are properly defined"
645
+
646
+
647
+ class FZPFamilyPropertyChecker(FZPChecker):
648
+ def check(self):
649
+ properties = self.fzp_doc.xpath("//property")
650
+ for prop in properties:
651
+ if prop.get('name') == 'family':
652
+ if not prop.text:
653
+ self.add_error("'family' property has no value.", node=prop)
654
+ return self.get_result()
655
+ self.add_error("'family' property is missing.", node=self.fzp_doc.getroot())
656
+ return self.get_result()
657
+
658
+ @staticmethod
659
+ def get_name():
660
+ return "family_property"
661
+
662
+ @staticmethod
663
+ def get_description():
664
+ return "Check family property is present"
665
+
666
+
667
+ class FZPUniquePropertyNamesChecker(FZPChecker):
668
+ def check(self):
669
+ properties = self.fzp_doc.xpath("//property")
670
+ names = set()
671
+ for prop in properties:
672
+ name = prop.get('name')
673
+ if name in names:
674
+ self.add_error(f"Duplicate property name found: '{name}'.", node=prop)
675
+ else:
676
+ names.add(name)
677
+ return self.get_result()
678
+
679
+ @staticmethod
680
+ def get_name():
681
+ return "unique_property_names"
682
+
683
+ @staticmethod
684
+ def get_description():
685
+ return "Check property names are unique"
686
+
687
+
688
+ class FZPPropertyFieldsChecker(FZPChecker):
689
+ def check(self):
690
+ properties = self.fzp_doc.xpath("//property")
691
+ for prop in properties:
692
+ name = prop.get('name')
693
+ if not name:
694
+ self.add_error(f"Property with empty 'name' attribute found: {etree.tostring(prop, pretty_print=True).decode()}", node=prop)
695
+ elif not prop.text:
696
+ self.add_error(f"Property '{name}' has an empty value.", node=prop)
697
+ return self.get_result()
698
+
699
+ def fix(self, filename):
700
+ """Apply fixes for property issues using regex to avoid etree side effects."""
701
+ filename_base = os.path.basename(filename).lower()
702
+
703
+ # Determine manufacturer from filename (case-insensitive)
704
+ manufacturer = None
705
+ if "adafruit" in filename_base:
706
+ manufacturer = "Adafruit"
707
+ elif "infineon" in filename_base:
708
+ manufacturer = "Infineon"
709
+ elif "arduino" in filename_base:
710
+ manufacturer = "Arduino"
711
+ elif "sparkfun" in filename_base:
712
+ manufacturer = "SparkFun"
713
+ elif "espressif" in filename_base:
714
+ manufacturer = "Espressif"
715
+
716
+ with open(filename, 'r', encoding='UTF-8') as f:
717
+ content = f.read()
718
+
719
+ original_content = content
720
+
721
+ # Remove empty layer properties
722
+ empty_layer_pattern = r'[\t ]*<property name="layer">\s*</property>\r?\n?'
723
+ new_content, layer_removals = re.subn(empty_layer_pattern, '', content)
724
+ if layer_removals > 0:
725
+ self.add_fix(f"Removed {layer_removals} empty 'layer' property/properties")
726
+ content = new_content
727
+
728
+ if manufacturer:
729
+ # Set empty mn property to detected manufacturer
730
+ empty_mn_pattern = r'(<property name="mn">)\s*(</property>)'
731
+ mn_replacement = rf'\1{manufacturer}\2'
732
+ new_content, mn_fixes = re.subn(empty_mn_pattern, mn_replacement, content)
733
+ if mn_fixes > 0:
734
+ self.add_fix(f"Set empty 'mn' property to '{manufacturer}'")
735
+ content = new_content
736
+
737
+ # Find part number value to set mpn (if empty)
738
+ part_number_match = re.search(r'<property name="part number">([^<]+)</property>', content)
739
+ if part_number_match:
740
+ part_number_value = part_number_match.group(1).strip()
741
+
742
+ # Set empty mpn property to part number value
743
+ empty_mpn_pattern = r'<property name="mpn">\s*</property>'
744
+ mpn_replacement = f'<property name="mpn">{part_number_value}</property>'
745
+ new_content, mpn_fixes = re.subn(empty_mpn_pattern, mpn_replacement, content)
746
+ if mpn_fixes > 0:
747
+ self.add_fix(f"Set 'mpn' property to '{part_number_value}' from 'part number'")
748
+ content = new_content
749
+
750
+ # Write back if modified
751
+ if content != original_content:
752
+ with open(filename, 'w', encoding='UTF-8') as f:
753
+ f.write(content)
754
+ return True
755
+
756
+ return False
757
+
758
+ @staticmethod
759
+ def get_name():
760
+ return "property_fields"
761
+
762
+ @staticmethod
763
+ def get_description():
764
+ return "Check property fields are properly defined"
765
+
766
+
767
+ class FZPRequiredTagsChecker(FZPChecker):
768
+ def check(self):
769
+ required_attributes = {
770
+ 'module': ['moduleId']
771
+ }
772
+ required_tags = ['title', 'tags', 'properties', 'views', 'connectors']
773
+
774
+ # Check required attributes
775
+ for element, attributes in required_attributes.items():
776
+ elements = self.fzp_doc.xpath(f"//{element}")
777
+ if elements:
778
+ for attr in attributes:
779
+ if not elements[0].get(attr):
780
+ self.add_error(f"Tag '{element}' is missing required attribute '{attr}'.", node=elements[0])
781
+
782
+ # Check required tags
783
+ for tag in required_tags:
784
+ if not self.fzp_doc.xpath(f"//{tag}"):
785
+ self.add_error(f"Required tag '{tag}' is missing.", node=self.fzp_doc.getroot())
786
+
787
+ return self.get_result()
788
+
789
+ @staticmethod
790
+ def get_name():
791
+ return "required_tags"
792
+
793
+ @staticmethod
794
+ def get_description():
795
+ return "Check all required tags and attributes are present"
796
+
797
+
798
+ class FZPBusesChecker(FZPChecker):
799
+ def check(self):
800
+ buses = self.fzp_doc.xpath("//bus")
801
+ for bus in buses:
802
+ bus_id = bus.get('id')
803
+ if not bus_id:
804
+ self.add_error(f"Bus found without an ID: {etree.tostring(bus, pretty_print=True).decode()}")
805
+
806
+ node_members = bus.xpath(".//nodeMember")
807
+ if not node_members:
808
+ if bus_id:
809
+ self.add_error(f"Bus '{bus_id}' has no node members.")
810
+ else:
811
+ self.add_error("Bus has no node members.")
812
+
813
+ return self.get_result()
814
+
815
+ @staticmethod
816
+ def get_name():
817
+ return "buses"
818
+
819
+ @staticmethod
820
+ def get_description():
821
+ return "Check buses are properly defined"
822
+
823
+ class FZPLayerIDsChecker(FZPChecker):
824
+ def __init__(self, fzp_doc, svg_docs):
825
+ super().__init__(fzp_doc)
826
+ self.svg_docs = svg_docs
827
+
828
+ def check(self):
829
+ views = self.fzp_doc.xpath("//views")[0]
830
+ for view in views:
831
+ if view.tag == "defaultUnits":
832
+ continue
833
+
834
+ layers_elements = view.xpath("layers")
835
+ if not layers_elements:
836
+ continue
837
+
838
+ layers = layers_elements[0]
839
+ image = layers.get("image")
840
+ if not image:
841
+ continue
842
+
843
+ svg_doc = self.svg_docs.get(view.tag)
844
+ if not svg_doc:
845
+ continue # Skip if SVG not available
846
+
847
+ # Check each layer ID
848
+ layer_elements = layers.xpath("layer")
849
+ for layer_element in layer_elements:
850
+ layer_id = layer_element.get("layerId")
851
+ if not layer_id:
852
+ continue
853
+
854
+ # Look for matching ID in SVG
855
+ matching_elements = svg_doc.xpath(f"//*[@id='{layer_id}']")
856
+ if not matching_elements:
857
+ self.add_error(f"Layer ID '{layer_id}' from {view.tag} not found in SVG")
858
+
859
+ return self.get_result()
860
+
861
+ @staticmethod
862
+ def get_name():
863
+ return "layer_ids"
864
+
865
+ @staticmethod
866
+ def get_description():
867
+ return "Check that layer IDs in FZP file match with IDs in corresponding SVG files"
868
+
869
+
870
+ class FZPDateFormatChecker(FZPChecker):
871
+ """Checker for date format validation and automatic fixing"""
872
+
873
+ def __init__(self, fzp_doc):
874
+ super().__init__(fzp_doc)
875
+
876
+ def check(self):
877
+ """Check date format in FZP file"""
878
+ # Find all date elements
879
+ date_elements = self.fzp_doc.xpath("//date")
880
+
881
+ for date_element in date_elements:
882
+ if date_element.text is None or not date_element.text.strip():
883
+ self.add_warning("Date element is empty", node=date_element)
884
+ continue
885
+
886
+ date_text = date_element.text.strip()
887
+
888
+ # Try to parse as ISO format first (this is the target format)
889
+ try:
890
+ datetime.date.fromisoformat(date_text)
891
+ # If successful, date is already in correct format
892
+ continue
893
+ except ValueError:
894
+ pass
895
+
896
+ # Try to parse and fix common date formats
897
+ converted_date = self._try_convert_date_format(date_text)
898
+
899
+ if converted_date:
900
+ # Report the issue - fix method will handle the actual fixing
901
+ self.add_warning(f"Date format '{date_text}' should be in YYYY-MM-DD format. Can be converted to: '{converted_date}'",
902
+ node=date_element)
903
+ else:
904
+ # Unable to parse the date
905
+ self.add_error(f"Invalid date format: '{date_text}'. Expected YYYY-MM-DD format.",
906
+ node=date_element)
907
+
908
+ return self.get_result()
909
+
910
+ def fix(self, filename):
911
+ """Apply date format fixes using regex to avoid etree side effects"""
912
+ with open(filename, 'r', encoding='UTF-8') as f:
913
+ content = f.read()
914
+
915
+ original_content = content
916
+
917
+ # Apply date format conversions using regex
918
+ fixes_applied = 0
919
+
920
+ # Pattern to match date elements
921
+ date_pattern = r'(<date>)(.*?)(</date>)'
922
+
923
+ def date_replacer(match):
924
+ nonlocal fixes_applied
925
+ date_text = match.group(2).strip()
926
+
927
+ # Skip if already in ISO format
928
+ try:
929
+ datetime.date.fromisoformat(date_text)
930
+ return match.group(0) # Return unchanged
931
+ except ValueError:
932
+ pass
933
+
934
+ # Try to convert the date
935
+ converted_date = self._try_convert_date_format(date_text)
936
+ if converted_date:
937
+ fixes_applied += 1
938
+ self.add_fix(f"Converted date format from '{date_text}' to '{converted_date}'")
939
+ return f"{match.group(1)}{converted_date}{match.group(3)}"
940
+
941
+ return match.group(0) # Return unchanged if can't convert
942
+
943
+ new_content = re.sub(date_pattern, date_replacer, content)
944
+
945
+ # Write back if changes were made
946
+ if new_content != original_content:
947
+ with open(filename, 'w', encoding='UTF-8') as f:
948
+ f.write(new_content)
949
+
950
+ return self.fixes
951
+
952
+ def _try_convert_date_format(self, date_text):
953
+ """Try to convert various date formats to ISO format (YYYY-MM-DD)"""
954
+
955
+ # Common date format patterns and their conversion logic
956
+ date_patterns = [
957
+ # Format: "Thu Jun 13 2024" or "Jun 13 2024" (with optional day name)
958
+ {
959
+ 'pattern': r'^(?:\w{3}\s+)?(\w{3})\s+(\d{1,2})\s+(\d{4})$',
960
+ 'months': {
961
+ 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
962
+ 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
963
+ },
964
+ 'converter': self._convert_month_day_year
965
+ },
966
+
967
+ # Format: "13/06/2024" or "13-06-2024" (ambiguous DD/MM/YYYY vs MM/DD/YYYY)
968
+ # We'll try both interpretations and use the valid one
969
+ {
970
+ 'pattern': r'^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$',
971
+ 'converter': self._convert_ambiguous_date
972
+ },
973
+
974
+ # Format: "2024/06/13" or "2024-06-13" (YYYY/MM/DD or YYYY-MM-DD with wrong separator)
975
+ {
976
+ 'pattern': r'^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$',
977
+ 'converter': self._convert_yyyy_mm_dd_fix_separator
978
+ },
979
+
980
+ # Japanese format: "木 3 31 2016" (from importer)
981
+ {
982
+ 'pattern': r'^木\s+(\d+)\s+(\d+)\s+(\d+)$',
983
+ 'converter': self._convert_japanese_format
984
+ }
985
+ ]
986
+
987
+ for pattern_info in date_patterns:
988
+ match = re.match(pattern_info['pattern'], date_text)
989
+ if match:
990
+ try:
991
+ return pattern_info['converter'](match, pattern_info.get('months'))
992
+ except (ValueError, KeyError):
993
+ continue
994
+
995
+ return None
996
+
997
+ def _convert_month_day_year(self, match, months_dict):
998
+ """Convert 'Thu Jun 13 2024' or 'Jun 13 2024' format"""
999
+ month_name = match.group(1)
1000
+ day = int(match.group(2))
1001
+ year = int(match.group(3))
1002
+
1003
+ if month_name not in months_dict:
1004
+ raise ValueError(f"Unknown month: {month_name}")
1005
+
1006
+ month = months_dict[month_name]
1007
+ return f"{year:04d}-{month}-{day:02d}"
1008
+
1009
+ def _convert_ambiguous_date(self, match, months_dict=None):
1010
+ """Convert DD/MM/YYYY or MM/DD/YYYY format - try both interpretations"""
1011
+ first_num = int(match.group(1))
1012
+ second_num = int(match.group(2))
1013
+ year = int(match.group(3))
1014
+
1015
+ # Try DD/MM/YYYY first (European format)
1016
+ if 1 <= second_num <= 12 and 1 <= first_num <= 31:
1017
+ day, month = first_num, second_num
1018
+ # Additional validation: check for obviously wrong dates
1019
+ if day > 12 and month <= 12:
1020
+ # Definitely DD/MM format (e.g., 25/06/2024)
1021
+ return f"{year:04d}-{month:02d}-{day:02d}"
1022
+
1023
+ # Try MM/DD/YYYY (US format)
1024
+ if 1 <= first_num <= 12 and 1 <= second_num <= 31:
1025
+ month, day = first_num, second_num
1026
+ # Additional validation: check for obviously wrong dates
1027
+ if first_num > 12:
1028
+ # Can't be MM/DD format
1029
+ raise ValueError("Invalid date format")
1030
+ elif second_num > 12:
1031
+ # Must be MM/DD format (e.g., 06/25/2024)
1032
+ return f"{year:04d}-{month:02d}-{day:02d}"
1033
+
1034
+ # If both are valid (ambiguous case like 05/06/2024), default to DD/MM
1035
+ if 1 <= second_num <= 12 and 1 <= first_num <= 12:
1036
+ day, month = first_num, second_num
1037
+ return f"{year:04d}-{month:02d}-{day:02d}"
1038
+
1039
+ raise ValueError("Invalid day or month")
1040
+
1041
+ def _convert_yyyy_mm_dd_fix_separator(self, match, months_dict=None):
1042
+ """Convert YYYY/MM/DD or YYYY-MM-DD with wrong separator to YYYY-MM-DD"""
1043
+ year = int(match.group(1))
1044
+ month = int(match.group(2))
1045
+ day = int(match.group(3))
1046
+
1047
+ if not (1 <= month <= 12) or not (1 <= day <= 31):
1048
+ raise ValueError("Invalid day or month")
1049
+
1050
+ return f"{year:04d}-{month:02d}-{day:02d}"
1051
+
1052
+ def _convert_japanese_format(self, match, months_dict=None):
1053
+ """Convert Japanese format '木 3 31 2016' to ISO format"""
1054
+ month = int(match.group(1))
1055
+ day = int(match.group(2))
1056
+ year = int(match.group(3))
1057
+
1058
+ if not (1 <= month <= 12) or not (1 <= day <= 31):
1059
+ raise ValueError("Invalid day or month")
1060
+
1061
+ return f"{year:04d}-{month:02d}-{day:02d}"
1062
+
1063
+ @staticmethod
1064
+ def get_name():
1065
+ return "date_format"
1066
+
1067
+ @staticmethod
1068
+ def get_description():
1069
+ return "Check and fix date format in FZP files. Supports multiple common date formats and converts them to ISO format (YYYY-MM-DD)"