fritzkit 0.0.0-stage → 0.1.1
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.
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +13 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +429 -2
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +7 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +17 -0
- package/dist/cli/commands/export.js +81 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/kicad.d.ts +10 -0
- package/dist/cli/commands/kicad.js +22 -0
- package/dist/cli/commands/kicad.js.map +1 -0
- package/dist/cli/commands/parts.d.ts +22 -0
- package/dist/cli/commands/parts.js +46 -0
- package/dist/cli/commands/parts.js.map +1 -0
- package/dist/cli/commands/prune.d.ts +14 -0
- package/dist/cli/commands/prune.js +67 -0
- package/dist/cli/commands/prune.js.map +1 -0
- package/dist/cli/commands/render.d.ts +24 -0
- package/dist/cli/commands/render.js +62 -0
- package/dist/cli/commands/render.js.map +1 -0
- package/dist/cli/commands/schema.d.ts +10 -0
- package/dist/cli/commands/schema.js +35 -0
- package/dist/cli/commands/schema.js.map +1 -0
- package/dist/cli/commands/ui.d.ts +14 -0
- package/dist/cli/commands/ui.js +67 -0
- package/dist/cli/commands/ui.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +24 -0
- package/dist/cli/commands/validate.js +91 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/context.d.ts +81 -0
- package/dist/cli/context.js +139 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +8 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/program.d.ts +21 -0
- package/dist/cli/program.js +158 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/raster.d.ts +4 -0
- package/dist/cli/raster.js +9 -0
- package/dist/cli/raster.js.map +1 -0
- package/dist/core/compile/breadboard-util.d.ts +12 -0
- package/dist/core/compile/breadboard-util.js +107 -0
- package/dist/core/compile/breadboard-util.js.map +1 -0
- package/dist/core/compile/breadboard.d.ts +60 -0
- package/dist/core/compile/breadboard.js +513 -0
- package/dist/core/compile/breadboard.js.map +1 -0
- package/dist/core/compile/fzp.d.ts +12 -0
- package/dist/core/compile/fzp.js +183 -0
- package/dist/core/compile/fzp.js.map +1 -0
- package/dist/core/compile/icon.d.ts +17 -0
- package/dist/core/compile/icon.js +27 -0
- package/dist/core/compile/icon.js.map +1 -0
- package/dist/core/compile/index.d.ts +4 -0
- package/dist/core/compile/index.js +62 -0
- package/dist/core/compile/index.js.map +1 -0
- package/dist/core/compile/pcb.d.ts +39 -0
- package/dist/core/compile/pcb.js +164 -0
- package/dist/core/compile/pcb.js.map +1 -0
- package/dist/core/compile/schematic.d.ts +69 -0
- package/dist/core/compile/schematic.js +240 -0
- package/dist/core/compile/schematic.js.map +1 -0
- package/dist/core/compile/style.d.ts +52 -0
- package/dist/core/compile/style.js +72 -0
- package/dist/core/compile/style.js.map +1 -0
- package/dist/core/compile/types.d.ts +50 -0
- package/dist/core/compile/types.js +15 -0
- package/dist/core/compile/types.js.map +1 -0
- package/dist/core/export/bin.d.ts +61 -0
- package/dist/core/export/bin.js +221 -0
- package/dist/core/export/bin.js.map +1 -0
- package/dist/core/export/fzpz.d.ts +37 -0
- package/dist/core/export/fzpz.js +121 -0
- package/dist/core/export/fzpz.js.map +1 -0
- package/dist/core/export/index.d.ts +5 -0
- package/dist/core/export/index.js +6 -0
- package/dist/core/export/index.js.map +1 -0
- package/dist/core/export/install.d.ts +77 -0
- package/dist/core/export/install.js +183 -0
- package/dist/core/export/install.js.map +1 -0
- package/dist/core/export/prune.d.ts +72 -0
- package/dist/core/export/prune.js +165 -0
- package/dist/core/export/prune.js.map +1 -0
- package/dist/core/fritzing.d.ts +22 -0
- package/dist/core/fritzing.js +77 -0
- package/dist/core/fritzing.js.map +1 -0
- package/dist/core/identity.d.ts +29 -0
- package/dist/core/identity.js +70 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/image/bbox.d.ts +27 -0
- package/dist/core/image/bbox.js +24 -0
- package/dist/core/image/bbox.js.map +1 -0
- package/dist/core/image/index.d.ts +8 -0
- package/dist/core/image/index.js +9 -0
- package/dist/core/image/index.js.map +1 -0
- package/dist/core/image/key.d.ts +72 -0
- package/dist/core/image/key.js +317 -0
- package/dist/core/image/key.js.map +1 -0
- package/dist/core/image/prepare.d.ts +31 -0
- package/dist/core/image/prepare.js +98 -0
- package/dist/core/image/prepare.js.map +1 -0
- package/dist/core/image/remover.d.ts +17 -0
- package/dist/core/image/remover.js +51 -0
- package/dist/core/image/remover.js.map +1 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +17 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/kicad/footprint.d.ts +104 -0
- package/dist/core/kicad/footprint.js +317 -0
- package/dist/core/kicad/footprint.js.map +1 -0
- package/dist/core/kicad/index.d.ts +25 -0
- package/dist/core/kicad/index.js +197 -0
- package/dist/core/kicad/index.js.map +1 -0
- package/dist/core/kicad/sexpr.d.ts +35 -0
- package/dist/core/kicad/sexpr.js +163 -0
- package/dist/core/kicad/sexpr.js.map +1 -0
- package/dist/core/resolve.d.ts +113 -0
- package/dist/core/resolve.js +301 -0
- package/dist/core/resolve.js.map +1 -0
- package/dist/core/schema/part.d.ts +995 -0
- package/dist/core/schema/part.js +278 -0
- package/dist/core/schema/part.js.map +1 -0
- package/dist/core/schema/part.schema.json +1122 -0
- package/dist/core/svg.d.ts +20 -0
- package/dist/core/svg.js +56 -0
- package/dist/core/svg.js.map +1 -0
- package/dist/core/units.d.ts +28 -0
- package/dist/core/units.js +42 -0
- package/dist/core/units.js.map +1 -0
- package/dist/core/validate/checker.d.ts +46 -0
- package/dist/core/validate/checker.js +142 -0
- package/dist/core/validate/checker.js.map +1 -0
- package/dist/core/validate/doctor.d.ts +25 -0
- package/dist/core/validate/doctor.js +156 -0
- package/dist/core/validate/doctor.js.map +1 -0
- package/dist/core/validate/explain.d.ts +10 -0
- package/dist/core/validate/explain.js +135 -0
- package/dist/core/validate/explain.js.map +1 -0
- package/dist/core/validate/index.d.ts +21 -0
- package/dist/core/validate/index.js +96 -0
- package/dist/core/validate/index.js.map +1 -0
- package/dist/core/validate/structural.d.ts +74 -0
- package/dist/core/validate/structural.js +664 -0
- package/dist/core/validate/structural.js.map +1 -0
- package/dist/core/workspace.d.ts +69 -0
- package/dist/core/workspace.js +213 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/mcp/jsonpatch.d.ts +30 -0
- package/dist/mcp/jsonpatch.js +145 -0
- package/dist/mcp/jsonpatch.js.map +1 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +5 -0
- package/dist/mcp/main.js.map +1 -0
- package/dist/mcp/server.d.ts +28 -0
- package/dist/mcp/server.js +310 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/server/api.d.ts +72 -0
- package/dist/server/api.js +507 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/events.d.ts +28 -0
- package/dist/server/events.js +134 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/http.d.ts +36 -0
- package/dist/server/http.js +127 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +65 -0
- package/dist/server/index.js +193 -0
- package/dist/server/index.js.map +1 -0
- package/dist/ui/assets/index-DLU8opyI.js +63 -0
- package/dist/ui/assets/index-GhUA-T3b.css +1 -0
- package/dist/ui/index.html +14 -0
- package/package.json +89 -4
- package/skills/fritzkit/SKILL.md +690 -0
- package/skills/fritzkit/reference/examples.md +275 -0
- package/skills/fritzkit/reference/part-json.md +333 -0
- package/skills/fritzkit/scripts/measure.py +197 -0
- package/tools/fzp-checker/.python-version +1 -0
- package/tools/fzp-checker/README.md +45 -0
- package/tools/fzp-checker/check_fzp.py +48 -0
- package/tools/fzp-checker/pyproject.toml +26 -0
- package/tools/fzp-checker/uv.lock +266 -0
- package/tools/fzp-checker/vendor/LICENSE.txt +8 -0
- package/tools/fzp-checker/vendor/UPSTREAM.md +25 -0
- package/tools/fzp-checker/vendor/fzp_checker.py +14 -0
- package/tools/fzp-checker/vendor/scripts/checks/__init__.py +26 -0
- package/tools/fzp-checker/vendor/scripts/checks/check_missing_leg_ids.py +44 -0
- package/tools/fzp-checker/vendor/scripts/checks/checkascii.py +27 -0
- package/tools/fzp-checker/vendor/scripts/checks/checkcase.py +156 -0
- package/tools/fzp-checker/vendor/scripts/checks/checkcopies.py +104 -0
- package/tools/fzp-checker/vendor/scripts/checks/connector_numbering.py +79 -0
- package/tools/fzp-checker/vendor/scripts/checks/connectors_misnumbered.py +228 -0
- package/tools/fzp-checker/vendor/scripts/checks/explain_errors.md +95 -0
- package/tools/fzp-checker/vendor/scripts/checks/fzp_checker_runner.py +717 -0
- package/tools/fzp-checker/vendor/scripts/checks/fzp_checkers.py +1069 -0
- package/tools/fzp-checker/vendor/scripts/checks/fzp_svg_checkers.py +62 -0
- package/tools/fzp-checker/vendor/scripts/checks/fzp_utils.py +222 -0
- package/tools/fzp-checker/vendor/scripts/checks/inspector_fc51.png +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/invisibleconnectors.py +118 -0
- package/tools/fzp-checker/vendor/scripts/checks/nolayeridcheck.txt +758 -0
- package/tools/fzp-checker/vendor/scripts/checks/requirements.txt +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/svg_checkers.py +953 -0
- package/tools/fzp-checker/vendor/scripts/checks/svg_utils.py +96 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_checkers.py +839 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/author_missing.fzp.test +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/author_present.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/buses_missing_id.fzp.test +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/buses_missing_node_members.fzp.test +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/buses_present_valid.fzp.test +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_layers_missing_attributes.fzp.test +16 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_layers_present_valid.fzp.test +14 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_numbering_invalid.fzp.test +70 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_numbering_valid.fzp.test +70 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/connector_refs_valid.fzp.test +53 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_mixed.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_silkscreen_nested.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_style_attr.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_white_stroke.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_invalid_wrong_color.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_inherited.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_legacy.fzp.test +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_nested_copper.fzp.test +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_none.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_standard.fzp.test +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/copper_color_valid_uppercase.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/css_connector.fzp.test +100 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/description_missing.fzp.test +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/description_present.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/duplicate_ids.fzp.test +27 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/family_property_missing.fzp.test +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/family_property_present_valid.fzp.test +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/font_size.fzp.test +46 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/fritzing_version_invalid_format.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/fritzing_version_missing.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/fritzing_version_present_valid.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/gorn_absent.fzp.test +15 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/gorn_present.fzp.test +15 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/hybrid_connectors.fzp.test +232 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/invalid_xml.fzp.test +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_ids_match.fzp.test +29 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_ids_mismatch.fzp.test +30 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_nesting_invalid.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/layer_nesting_valid.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/matrix_transform.fzp.test +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/missing_connector_refs.fzp.test +63 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/missing_leg_ids.fzp.test +34 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_missing.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_non_alnum.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_present.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_special_chars_absent.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_special_chars_present.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/module_id_too_short.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/no_layer_invalid.fzp.test +19 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/no_layer_namedview.fzp.test +19 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/no_layer_valid.fzp.test +19 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/pcb_only.fzp.test +48 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/property_fields_missing_name.fzp.test +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/property_fields_missing_value.fzp.test +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/property_fields_present.fzp.test +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/required_tags_missing.fzp.test +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/required_tags_present.fzp.test +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/stroke_test.fzp.test +60 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/template_svgs.fzp.test +35 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/title_missing.fzp.test +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/title_present.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/unique_ids.fzp.test +27 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/unique_property_names_duplicates.fzp.test +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/unique_property_names_unique.fzp.test +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/valid_xml.fzp.test +249 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/version_invalid_format.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/version_missing.fzp.test +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/version_present_valid.fzp.test +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/views_missing.fzp.test +3 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/core/views_present.fzp.test +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/connector_refs_valid_breadboard.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/duplicate_ids_breadboard.svg +13 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/gorn_absent_breadboard.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/gorn_present_breadboard.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/hybrid_connectors_breadboard.svg +102 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/layer_nesting_invalid.svg +13 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/layer_nesting_valid.svg +13 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/matrix_transform.svg +11 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/missing_connector_refs_breadboard.svg +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/missing_leg_ids_breadboard.svg +9 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/test_layer_ids_match_breadboard.svg +9 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/test_layer_ids_mismatch_breadboard.svg +9 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/breadboard/unique_ids_breadboard.svg +13 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/hybrid_connectors_icon.svg +78 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/no_layer_invalid.svg +4 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/no_layer_namedview.svg +9 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/no_layer_valid.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/icon/plain_pcb.svg +10 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/connector_refs_valid_pcb.svg +10 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_mixed.svg +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_silkscreen_nested.svg +10 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_style_attr.svg +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_white_stroke.svg +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_invalid_wrong_color.svg +6 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_inherited.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_legacy.svg +10 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_nested_copper.svg +9 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_none.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_standard.svg +10 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/copper_color_valid_uppercase.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/css_connector.svg +25 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/font_size.svg +54 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/hybrid_connectors_pcb.svg +35 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/missing_connector_refs_pcb.svg +14 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/pcb-arduino-shield.svg +25 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/stroke_test.svg +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/test_layer_ids_match_pcb.svg +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/pcb/test_layer_ids_mismatch_pcb.svg +12 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/connector_refs_valid_schematic.svg +7 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/hybrid_connectors_schematic.svg +59 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/missing_connector_refs_schematic.svg +8 -0
- package/tools/fzp-checker/vendor/scripts/checks/test_data/svg/core/schematic/test_layer_ids_match_schematic.svg +6 -0
- 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)"
|