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.
- 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/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/check_missing_leg_ids.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/connector_numbering.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_checker_runner.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_checkers.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_svg_checkers.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/fzp_utils.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/svg_checkers.cpython-313.pyc +0 -0
- package/tools/fzp-checker/vendor/scripts/checks/__pycache__/svg_utils.cpython-313.pyc +0 -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,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validator: pure checks over a compiled part (fzp text + one SVG per view +
|
|
3
|
+
* the flat zip entry list). Catches the mistakes Fritzing either rejects on import or
|
|
4
|
+
* silently mis-renders, before the (slower, Python) vendored part checker runs.
|
|
5
|
+
* No fs, no network.
|
|
6
|
+
*/
|
|
7
|
+
import { XMLParser } from "fast-xml-parser";
|
|
8
|
+
import { VIEWS } from "../compile/types.js";
|
|
9
|
+
export const ALLOWED_FONTS = ["Noto Sans", "OCR-Fritzing-mono"];
|
|
10
|
+
const VIEW_TAG = {
|
|
11
|
+
breadboard: "breadboardView",
|
|
12
|
+
schematic: "schematicView",
|
|
13
|
+
pcb: "pcbView",
|
|
14
|
+
icon: "iconView",
|
|
15
|
+
};
|
|
16
|
+
const ARRAY_TAGS = new Set([
|
|
17
|
+
"connector",
|
|
18
|
+
"layer",
|
|
19
|
+
"layers",
|
|
20
|
+
"p",
|
|
21
|
+
"property",
|
|
22
|
+
"bus",
|
|
23
|
+
"nodeMember",
|
|
24
|
+
"line",
|
|
25
|
+
"tag",
|
|
26
|
+
"model",
|
|
27
|
+
]);
|
|
28
|
+
function asObj(v) {
|
|
29
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
30
|
+
}
|
|
31
|
+
function asArr(v) {
|
|
32
|
+
if (Array.isArray(v))
|
|
33
|
+
return v.map((x) => asObj(x) ?? { "#text": String(x) });
|
|
34
|
+
const o = asObj(v);
|
|
35
|
+
if (o)
|
|
36
|
+
return [o];
|
|
37
|
+
if (v === undefined || v === null)
|
|
38
|
+
return [];
|
|
39
|
+
return [{ "#text": String(v) }];
|
|
40
|
+
}
|
|
41
|
+
function textOf(v) {
|
|
42
|
+
if (v === undefined || v === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
45
|
+
return String(v);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(v))
|
|
48
|
+
return textOf(v[0]);
|
|
49
|
+
const o = asObj(v);
|
|
50
|
+
if (o) {
|
|
51
|
+
const t = o["#text"];
|
|
52
|
+
return t === undefined ? "" : String(t);
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function attr(o, name) {
|
|
57
|
+
const v = o?.[`@_${name}`];
|
|
58
|
+
return v === undefined ? undefined : String(v);
|
|
59
|
+
}
|
|
60
|
+
/** Parse an fzp string. Throws on malformed XML. */
|
|
61
|
+
export function parseFzp(fzp) {
|
|
62
|
+
const parser = new XMLParser({
|
|
63
|
+
ignoreAttributes: false,
|
|
64
|
+
attributeNamePrefix: "@_",
|
|
65
|
+
parseAttributeValue: false,
|
|
66
|
+
parseTagValue: false,
|
|
67
|
+
trimValues: true,
|
|
68
|
+
isArray: (name) => ARRAY_TAGS.has(name),
|
|
69
|
+
});
|
|
70
|
+
const doc = asObj(parser.parse(fzp, true));
|
|
71
|
+
const mod = asObj(doc?.module);
|
|
72
|
+
if (!mod)
|
|
73
|
+
throw new Error("fzp has no <module> root element");
|
|
74
|
+
const tags = {};
|
|
75
|
+
for (const t of ["title", "label", "date", "author", "version", "description"]) {
|
|
76
|
+
tags[t] = textOf(mod[t]);
|
|
77
|
+
}
|
|
78
|
+
const properties = [];
|
|
79
|
+
for (const props of asArr(mod.properties)) {
|
|
80
|
+
for (const p of asArr(props.property)) {
|
|
81
|
+
properties.push({ name: attr(p, "name") ?? "", value: (textOf(p) ?? "").trim() });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const views = {};
|
|
85
|
+
const viewsEl = asObj(mod.views);
|
|
86
|
+
for (const v of VIEWS) {
|
|
87
|
+
const ve = asObj(viewsEl?.[VIEW_TAG[v]]);
|
|
88
|
+
if (!ve)
|
|
89
|
+
continue;
|
|
90
|
+
const layersEls = asArr(ve.layers);
|
|
91
|
+
const first = layersEls[0];
|
|
92
|
+
views[v] = {
|
|
93
|
+
image: attr(first, "image"),
|
|
94
|
+
layers: layersEls.flatMap((le) => asArr(le.layer)
|
|
95
|
+
.map((l) => attr(l, "layerId"))
|
|
96
|
+
.filter((x) => x !== undefined)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const connectors = [];
|
|
100
|
+
for (const cs of asArr(mod.connectors)) {
|
|
101
|
+
for (const c of asArr(cs.connector)) {
|
|
102
|
+
const cviews = {};
|
|
103
|
+
const cv = asObj(c.views);
|
|
104
|
+
for (const v of VIEWS) {
|
|
105
|
+
const ve = asObj(cv?.[VIEW_TAG[v]]);
|
|
106
|
+
if (!ve)
|
|
107
|
+
continue;
|
|
108
|
+
cviews[v] = asArr(ve.p).map((p) => ({
|
|
109
|
+
layer: attr(p, "layer") ?? "",
|
|
110
|
+
svgId: attr(p, "svgId"),
|
|
111
|
+
terminalId: attr(p, "terminalId"),
|
|
112
|
+
legId: attr(p, "legId"),
|
|
113
|
+
hybrid: attr(p, "hybrid"),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
connectors.push({ id: attr(c, "id") ?? "", name: attr(c, "name"), views: cviews });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const buses = [];
|
|
120
|
+
for (const bs of asArr(mod.buses)) {
|
|
121
|
+
for (const b of asArr(bs.bus)) {
|
|
122
|
+
buses.push({
|
|
123
|
+
id: attr(b, "id") ?? "",
|
|
124
|
+
members: asArr(b.nodeMember).map((n) => attr(n, "connectorId") ?? ""),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const spiceLines = [];
|
|
129
|
+
for (const sp of asArr(mod.spice)) {
|
|
130
|
+
for (const l of asArr(sp.line))
|
|
131
|
+
spiceLines.push(textOf(l) ?? "");
|
|
132
|
+
for (const m of asArr(sp.model))
|
|
133
|
+
spiceLines.push(textOf(m) ?? "");
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
moduleId: attr(mod, "moduleId"),
|
|
137
|
+
fritzingVersion: attr(mod, "fritzingVersion"),
|
|
138
|
+
tags,
|
|
139
|
+
properties,
|
|
140
|
+
views,
|
|
141
|
+
connectors,
|
|
142
|
+
buses,
|
|
143
|
+
spiceLines,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Parse an SVG into a flat element list in document order. Throws on malformed XML. */
|
|
147
|
+
export function parseSvg(svg) {
|
|
148
|
+
const parser = new XMLParser({
|
|
149
|
+
ignoreAttributes: false,
|
|
150
|
+
attributeNamePrefix: "@_",
|
|
151
|
+
parseAttributeValue: false,
|
|
152
|
+
parseTagValue: false,
|
|
153
|
+
preserveOrder: true,
|
|
154
|
+
trimValues: true,
|
|
155
|
+
});
|
|
156
|
+
const tree = parser.parse(svg, true);
|
|
157
|
+
const out = [];
|
|
158
|
+
const walk = (items, ancestors) => {
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
const o = asObj(item);
|
|
161
|
+
if (!o)
|
|
162
|
+
continue;
|
|
163
|
+
const tag = Object.keys(o).find((k) => k !== ":@");
|
|
164
|
+
if (!tag || tag.startsWith("?") || tag.startsWith("#"))
|
|
165
|
+
continue;
|
|
166
|
+
const rawAttrs = asObj(o[":@"]) ?? {};
|
|
167
|
+
const attrs = {};
|
|
168
|
+
for (const [k, v] of Object.entries(rawAttrs)) {
|
|
169
|
+
if (k.startsWith("@_"))
|
|
170
|
+
attrs[k.slice(2)] = String(v);
|
|
171
|
+
}
|
|
172
|
+
const children = Array.isArray(o[tag]) ? o[tag] : [];
|
|
173
|
+
const text = children
|
|
174
|
+
.map((c) => {
|
|
175
|
+
const co = asObj(c);
|
|
176
|
+
return co && "#text" in co ? String(co["#text"]) : "";
|
|
177
|
+
})
|
|
178
|
+
.join("");
|
|
179
|
+
out.push({ tag: localName(tag), attrs, ancestors, text });
|
|
180
|
+
walk(children, [...ancestors, { tag: localName(tag), id: attrs.id }]);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
walk(tree, []);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
function localName(tag) {
|
|
187
|
+
const i = tag.indexOf(":");
|
|
188
|
+
return i >= 0 ? tag.slice(i + 1) : tag;
|
|
189
|
+
}
|
|
190
|
+
function num(v) {
|
|
191
|
+
if (v === undefined)
|
|
192
|
+
return undefined;
|
|
193
|
+
const n = Number.parseFloat(v);
|
|
194
|
+
return Number.isFinite(n) ? n : undefined;
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// checks
|
|
198
|
+
const CONNECTOR_RE = /^connector(\d+)$/;
|
|
199
|
+
const CONNECTOR_ELEMENT_RE = /^(connector\d+)(pin|terminal|leg|pad)$/;
|
|
200
|
+
function err(code, message, where, fix) {
|
|
201
|
+
return { level: "error", code, message, ...(where ? { where } : {}), ...(fix ? { fix } : {}) };
|
|
202
|
+
}
|
|
203
|
+
function warn(code, message, where, fix) {
|
|
204
|
+
return { level: "warning", code, message, ...(where ? { where } : {}), ...(fix ? { fix } : {}) };
|
|
205
|
+
}
|
|
206
|
+
function checkModuleId(m) {
|
|
207
|
+
const issues = [];
|
|
208
|
+
const id = m.moduleId;
|
|
209
|
+
if (!id) {
|
|
210
|
+
issues.push(err("module-id", "module has no moduleId attribute", "fzp"));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
if (id.length < 8) {
|
|
214
|
+
issues.push(err("module-id", `moduleId "${id}" is shorter than 8 characters`, "fzp", "use a longer, unique id"));
|
|
215
|
+
}
|
|
216
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(id)) {
|
|
217
|
+
issues.push(err("module-id", `moduleId "${id}" contains characters outside [A-Za-z0-9_.-]`, "fzp", "the moduleId becomes a folder name; keep it to letters, digits, _ . -"));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!m.fritzingVersion) {
|
|
221
|
+
issues.push(err("module-id", "module has no fritzingVersion attribute", "fzp", 'add fritzingVersion="1.0.7"'));
|
|
222
|
+
}
|
|
223
|
+
return issues;
|
|
224
|
+
}
|
|
225
|
+
/** "breadboard/x.svg" -> "svg.breadboard.x.svg". */
|
|
226
|
+
export function entryNameForImage(image) {
|
|
227
|
+
return `svg.${image.replace(/[\\/]/g, ".")}`;
|
|
228
|
+
}
|
|
229
|
+
function checkZipEntries(m, input) {
|
|
230
|
+
const issues = [];
|
|
231
|
+
const names = input.entries.map((e) => e.name);
|
|
232
|
+
for (const n of names) {
|
|
233
|
+
if (/[\\/]/.test(n)) {
|
|
234
|
+
issues.push(err("zip-entries", `zip entry "${n}" is inside a folder`, n, "a .fzpz is flat: part.<id>.fzp and svg.<view>.<name>.svg at the root"));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const fzps = names.filter((n) => n.startsWith("part.") && n.endsWith(".fzp"));
|
|
238
|
+
if (fzps.length !== 1) {
|
|
239
|
+
issues.push(err("zip-entries", `expected exactly one part.*.fzp entry, found ${fzps.length}`, "zip", "a .fzpz holds one fzp named part.<moduleId>.fzp"));
|
|
240
|
+
}
|
|
241
|
+
const expected = new Set();
|
|
242
|
+
for (const v of VIEWS) {
|
|
243
|
+
const img = m.views[v]?.image;
|
|
244
|
+
if (img === undefined)
|
|
245
|
+
continue;
|
|
246
|
+
const want = entryNameForImage(img);
|
|
247
|
+
expected.add(want);
|
|
248
|
+
if (!names.includes(want)) {
|
|
249
|
+
issues.push(err("zip-entries", `${v}View image "${img}" has no matching zip entry "${want}"`, `${v}View`, "Fritzing maps svg.<view>.<file> to <view>/<file>; the names must match byte for byte"));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const n of names) {
|
|
253
|
+
if (n.startsWith("svg.") && n.endsWith(".svg") && !expected.has(n)) {
|
|
254
|
+
issues.push(err("zip-entries", `zip entry "${n}" is not referenced by any <layers image> in the fzp`, n, "remove the extra svg or fix the image attribute that should point at it"));
|
|
255
|
+
}
|
|
256
|
+
else if (!n.startsWith("svg.") && !(n.startsWith("part.") && n.endsWith(".fzp"))) {
|
|
257
|
+
issues.push(warn("zip-entries", `unexpected zip entry "${n}"`, n, "Fritzing ignores it"));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return issues;
|
|
261
|
+
}
|
|
262
|
+
function checkRequiredTags(m) {
|
|
263
|
+
const issues = [];
|
|
264
|
+
for (const t of ["title", "label", "date", "author", "version"]) {
|
|
265
|
+
const v = m.tags[t];
|
|
266
|
+
if (v === undefined || v.trim() === "") {
|
|
267
|
+
issues.push(err("required-tags", `missing or empty <${t}>`, "fzp", `add a <${t}> element`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const date = m.tags.date;
|
|
271
|
+
if (date && date.trim() !== "" && !/^\d{4}-\d{2}-\d{2}$/.test(date.trim())) {
|
|
272
|
+
issues.push(err("required-tags", `<date> "${date}" is not YYYY-MM-DD`, "fzp", "use an ISO date"));
|
|
273
|
+
}
|
|
274
|
+
const names = m.properties.map((p) => p.name.toLowerCase());
|
|
275
|
+
if (!names.includes("family")) {
|
|
276
|
+
issues.push(err("required-tags", 'properties have no "family"', "fzp", 'add <property name="family">; parts with the same family swap in the Inspector'));
|
|
277
|
+
}
|
|
278
|
+
if (!names.includes("variant")) {
|
|
279
|
+
issues.push(warn("required-tags", 'properties have no "variant"', "fzp", 'add <property name="variant"> so versions of the part are distinguishable'));
|
|
280
|
+
}
|
|
281
|
+
for (const p of m.properties) {
|
|
282
|
+
if (p.value === "") {
|
|
283
|
+
issues.push(err("required-tags", `property "${p.name}" has an empty value`, "fzp", "give it a value or remove it"));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return issues;
|
|
287
|
+
}
|
|
288
|
+
function idsOf(nodes) {
|
|
289
|
+
const m = new Map();
|
|
290
|
+
for (const n of nodes) {
|
|
291
|
+
const id = n.attrs.id;
|
|
292
|
+
if (id !== undefined && !m.has(id))
|
|
293
|
+
m.set(id, n);
|
|
294
|
+
}
|
|
295
|
+
return m;
|
|
296
|
+
}
|
|
297
|
+
function checkLayerIds(m, svgNodes) {
|
|
298
|
+
const issues = [];
|
|
299
|
+
for (const v of VIEWS) {
|
|
300
|
+
const view = m.views[v];
|
|
301
|
+
const nodes = svgNodes[v];
|
|
302
|
+
if (!view || !nodes)
|
|
303
|
+
continue;
|
|
304
|
+
const ids = idsOf(nodes);
|
|
305
|
+
for (const l of view.layers) {
|
|
306
|
+
if (!ids.has(l)) {
|
|
307
|
+
issues.push(err("layer-ids", `layer "${l}" listed in ${v}View has no element with id="${l}" in the ${v} svg`, `${v} svg`, `wrap the ${v} artwork in <g id="${l}">`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (v === "pcb") {
|
|
311
|
+
const c0 = ids.get("copper0");
|
|
312
|
+
if (c0 && ids.has("copper1") && !c0.ancestors.some((a) => a.id === "copper1")) {
|
|
313
|
+
issues.push(warn("layer-ids", "copper0 is not nested inside copper1", "pcb svg", 'nest <g id="copper0"> inside <g id="copper1"> so one THT pad element serves both layers'));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return issues;
|
|
318
|
+
}
|
|
319
|
+
function checkConnectorRefs(m, svgNodes) {
|
|
320
|
+
const issues = [];
|
|
321
|
+
const declared = new Set(m.connectors.map((c) => c.id));
|
|
322
|
+
for (const c of m.connectors) {
|
|
323
|
+
for (const v of VIEWS) {
|
|
324
|
+
const ps = c.views[v];
|
|
325
|
+
const nodes = svgNodes[v];
|
|
326
|
+
if (!ps || !nodes)
|
|
327
|
+
continue;
|
|
328
|
+
const ids = idsOf(nodes);
|
|
329
|
+
for (const p of ps) {
|
|
330
|
+
const where = `${c.id} ${v}View`;
|
|
331
|
+
if (p.terminalId && p.legId) {
|
|
332
|
+
issues.push(err("connector-refs", `${c.id} sets both terminalId and legId in ${v}View`, where, "a lead uses legId INSTEAD of a terminal; drop terminalId"));
|
|
333
|
+
}
|
|
334
|
+
if (p.hybrid === "yes")
|
|
335
|
+
continue;
|
|
336
|
+
for (const [kind, id] of [
|
|
337
|
+
["svgId", p.svgId],
|
|
338
|
+
["terminalId", p.terminalId],
|
|
339
|
+
["legId", p.legId],
|
|
340
|
+
]) {
|
|
341
|
+
if (id === undefined)
|
|
342
|
+
continue;
|
|
343
|
+
if (!ids.has(id)) {
|
|
344
|
+
issues.push(err("connector-refs", `${c.id} ${kind}="${id}" (layer ${p.layer}) not found in the ${v} svg`, where, `add an element with id="${id}" to the ${v} svg or fix the fzp reference`));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (p.svgId === undefined) {
|
|
348
|
+
issues.push(err("connector-refs", `${c.id} <p layer="${p.layer}"> in ${v}View has no svgId`, where, 'every <p> needs svgId="connectorNpin" (or hybrid="yes")'));
|
|
349
|
+
}
|
|
350
|
+
if (p.legId !== undefined) {
|
|
351
|
+
const n = ids.get(p.legId);
|
|
352
|
+
if (n && n.tag !== "line") {
|
|
353
|
+
issues.push(err("connector-refs", `${c.id} legId "${p.legId}" is a <${n.tag}>, not a <line>`, where, "Fritzing only bends legs drawn as a single <line>"));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (p.terminalId !== undefined) {
|
|
357
|
+
const n = ids.get(p.terminalId);
|
|
358
|
+
if (n && !hasArea(n)) {
|
|
359
|
+
issues.push(err("connector-refs", `${c.id} terminal "${p.terminalId}" (<${n.tag}>) has zero size`, where, "give the terminal a non-zero size, e.g. a 1 mil rect at the pin end"));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (const v of VIEWS) {
|
|
366
|
+
const nodes = svgNodes[v];
|
|
367
|
+
if (!nodes)
|
|
368
|
+
continue;
|
|
369
|
+
const reported = new Set();
|
|
370
|
+
for (const n of nodes) {
|
|
371
|
+
const mm = CONNECTOR_ELEMENT_RE.exec(n.attrs.id ?? "");
|
|
372
|
+
if (!mm || !mm[1] || declared.has(mm[1]) || reported.has(n.attrs.id ?? ""))
|
|
373
|
+
continue;
|
|
374
|
+
reported.add(n.attrs.id ?? "");
|
|
375
|
+
issues.push(warn("connector-refs", `element id="${n.attrs.id}" in the ${v} svg belongs to ${mm[1]}, which the fzp does not declare`, `${v} svg`, "remove the orphan element or declare the connector"));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return issues;
|
|
379
|
+
}
|
|
380
|
+
function hasArea(n) {
|
|
381
|
+
switch (n.tag) {
|
|
382
|
+
case "rect":
|
|
383
|
+
case "image":
|
|
384
|
+
case "use": {
|
|
385
|
+
const w = num(n.attrs.width);
|
|
386
|
+
const h = num(n.attrs.height);
|
|
387
|
+
return w !== undefined && h !== undefined && w > 0 && h > 0;
|
|
388
|
+
}
|
|
389
|
+
case "circle": {
|
|
390
|
+
const r = num(n.attrs.r);
|
|
391
|
+
return r !== undefined && r > 0;
|
|
392
|
+
}
|
|
393
|
+
case "ellipse": {
|
|
394
|
+
const rx = num(n.attrs.rx);
|
|
395
|
+
const ry = num(n.attrs.ry);
|
|
396
|
+
return rx !== undefined && ry !== undefined && rx > 0 && ry > 0;
|
|
397
|
+
}
|
|
398
|
+
case "line":
|
|
399
|
+
return false;
|
|
400
|
+
default:
|
|
401
|
+
// path/polygon/g: size cannot be computed cheaply; trust it.
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function checkConnectorNumbering(m) {
|
|
406
|
+
const issues = [];
|
|
407
|
+
const seen = new Set();
|
|
408
|
+
const nums = [];
|
|
409
|
+
for (const c of m.connectors) {
|
|
410
|
+
const mm = CONNECTOR_RE.exec(c.id);
|
|
411
|
+
if (!mm) {
|
|
412
|
+
issues.push(err("connector-numbering", `connector id "${c.id}" is not of the form connectorN`, c.id, "use connector0, connector1, ..."));
|
|
413
|
+
}
|
|
414
|
+
else if (!seen.has(c.id)) {
|
|
415
|
+
nums.push(Number(mm[1]));
|
|
416
|
+
}
|
|
417
|
+
if (seen.has(c.id)) {
|
|
418
|
+
issues.push(err("connector-numbering", `duplicate connector id "${c.id}"`, c.id, "every connector needs a unique id"));
|
|
419
|
+
}
|
|
420
|
+
seen.add(c.id);
|
|
421
|
+
}
|
|
422
|
+
nums.sort((a, b) => a - b);
|
|
423
|
+
const missing = [];
|
|
424
|
+
const max = nums[nums.length - 1];
|
|
425
|
+
if (max !== undefined) {
|
|
426
|
+
const have = new Set(nums);
|
|
427
|
+
for (let i = 0; i < max; i++)
|
|
428
|
+
if (!have.has(i))
|
|
429
|
+
missing.push(i);
|
|
430
|
+
}
|
|
431
|
+
if (missing.length > 0) {
|
|
432
|
+
issues.push(warn("connector-numbering", `connector numbers have gaps (missing ${missing.map((n) => `connector${n}`).join(", ")})`, "fzp", "expected when pins were removed: fritzkit never reuses a connector number (pinIds, D12a) " +
|
|
433
|
+
"so wires on the remaining pins stay attached when you swap to the new version"));
|
|
434
|
+
}
|
|
435
|
+
return issues;
|
|
436
|
+
}
|
|
437
|
+
function checkBuses(m) {
|
|
438
|
+
const issues = [];
|
|
439
|
+
const declared = new Set(m.connectors.map((c) => c.id));
|
|
440
|
+
const busIds = new Set();
|
|
441
|
+
const owner = new Map();
|
|
442
|
+
for (const b of m.buses) {
|
|
443
|
+
if (busIds.has(b.id)) {
|
|
444
|
+
issues.push(err("buses", `duplicate bus id "${b.id}"`, `bus ${b.id}`, "rename one bus"));
|
|
445
|
+
}
|
|
446
|
+
busIds.add(b.id);
|
|
447
|
+
for (const cid of b.members) {
|
|
448
|
+
if (!declared.has(cid)) {
|
|
449
|
+
issues.push(err("buses", `bus "${b.id}" references "${cid}", which is not a connector`, `bus ${b.id}`, "remove the nodeMember or fix the connector id"));
|
|
450
|
+
}
|
|
451
|
+
const prev = owner.get(cid);
|
|
452
|
+
if (prev !== undefined && prev !== b.id) {
|
|
453
|
+
issues.push(err("buses", `${cid} is in both bus "${prev}" and bus "${b.id}"`, `bus ${b.id}`, "merge the buses: a connector belongs to at most one bus"));
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
owner.set(cid, b.id);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (new Set(b.members).size < 2) {
|
|
460
|
+
issues.push(warn("buses", `bus "${b.id}" has fewer than 2 members`, `bus ${b.id}`, "a bus ties two or more connectors together; remove it"));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return issues;
|
|
464
|
+
}
|
|
465
|
+
const UNIT_RE = /^\s*([0-9]*\.?[0-9]+(?:[eE][-+]?\d+)?)\s*([a-z%]*)\s*$/;
|
|
466
|
+
function checkSvgRoot(v, nodes) {
|
|
467
|
+
const issues = [];
|
|
468
|
+
const where = `${v} svg`;
|
|
469
|
+
const root = nodes[0];
|
|
470
|
+
if (!root || root.tag !== "svg") {
|
|
471
|
+
return [err("svg-root", `the ${v} svg has no <svg> root element`, where)];
|
|
472
|
+
}
|
|
473
|
+
const dims = {};
|
|
474
|
+
for (const d of ["width", "height"]) {
|
|
475
|
+
const raw = root.attrs[d];
|
|
476
|
+
const mm = raw === undefined ? null : UNIT_RE.exec(raw);
|
|
477
|
+
if (!mm || !mm[1]) {
|
|
478
|
+
issues.push(err("svg-root", `the ${v} svg root has no valid ${d} (${raw ?? "missing"})`, where, `set ${d} in inches, e.g. ${d}="1.2in"`));
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const unit = mm[2] ?? "";
|
|
482
|
+
if (unit !== "in" && unit !== "mm") {
|
|
483
|
+
issues.push(err("svg-root", `the ${v} svg root ${d}="${raw}" has no in/mm unit`, where, "unitless or px sizes are read at 90 dpi and the part comes out the wrong size; use in or mm"));
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
dims[d] = { value: Number(mm[1]), unit };
|
|
487
|
+
}
|
|
488
|
+
const vb = root.attrs.viewBox;
|
|
489
|
+
const parts = vb === undefined
|
|
490
|
+
? []
|
|
491
|
+
: vb
|
|
492
|
+
.trim()
|
|
493
|
+
.split(/[\s,]+/)
|
|
494
|
+
.map(Number);
|
|
495
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
|
|
496
|
+
issues.push(err("svg-root", vb === undefined
|
|
497
|
+
? `the ${v} svg has no viewBox`
|
|
498
|
+
: `the ${v} svg viewBox "${vb}" is invalid`, where, 'add viewBox="0 0 <width in mils> <height in mils>"'));
|
|
499
|
+
return issues;
|
|
500
|
+
}
|
|
501
|
+
const vbSize = { width: parts[2] ?? 0, height: parts[3] ?? 0 };
|
|
502
|
+
for (const d of ["width", "height"]) {
|
|
503
|
+
const dim = dims[d];
|
|
504
|
+
if (!dim || dim.unit !== "in")
|
|
505
|
+
continue;
|
|
506
|
+
const want = dim.value * 1000;
|
|
507
|
+
if (want > 0 && Math.abs(vbSize[d] - want) / want > 0.005) {
|
|
508
|
+
issues.push(warn("svg-root", `the ${v} svg viewBox ${d} ${vbSize[d]} is not ${d}*1000 (${want})`, where, "1 unit should be 1 mil"));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return issues;
|
|
512
|
+
}
|
|
513
|
+
function checkSvgStyle(v, nodes) {
|
|
514
|
+
const issues = [];
|
|
515
|
+
const where = `${v} svg`;
|
|
516
|
+
const once = new Set();
|
|
517
|
+
const add = (key, issue) => {
|
|
518
|
+
if (once.has(key))
|
|
519
|
+
return;
|
|
520
|
+
once.add(key);
|
|
521
|
+
issues.push(issue);
|
|
522
|
+
};
|
|
523
|
+
for (const n of nodes) {
|
|
524
|
+
const label = n.attrs.id ? `<${n.tag} id="${n.attrs.id}">` : `<${n.tag}>`;
|
|
525
|
+
if (n.attrs.style !== undefined) {
|
|
526
|
+
add(`style:${label}`, err("svg-style", `${label} has a style= attribute`, where, "use XML attributes, Fritzing converts only a few CSS properties"));
|
|
527
|
+
}
|
|
528
|
+
if (n.tag === "tspan") {
|
|
529
|
+
add("tspan", err("svg-style", `the ${v} svg uses <tspan>`, where, "use one <text> element per line; Fritzing mis-places tspans"));
|
|
530
|
+
}
|
|
531
|
+
if (n.tag === "linearGradient" || n.tag === "radialGradient") {
|
|
532
|
+
add(`grad:${n.tag}`, err("svg-style", `the ${v} svg uses <${n.tag}>`, where, "use flat fills; gradients do not render reliably in Fritzing"));
|
|
533
|
+
}
|
|
534
|
+
if (Object.keys(n.attrs).some((k) => k === "gorn" || k.endsWith(":gorn"))) {
|
|
535
|
+
add("gorn", err("svg-style", `the ${v} svg has gorn attributes (first on ${label})`, where, "remove gorn attributes; they are Fritzing-internal and break re-import"));
|
|
536
|
+
}
|
|
537
|
+
if (n.tag === "image") {
|
|
538
|
+
// The icon is the breadboard artwork scaled down (compile/icon.ts), so a photo-style
|
|
539
|
+
// part embeds its photo in both; the Fritzing checker accepts that.
|
|
540
|
+
if (v !== "breadboard" && v !== "icon") {
|
|
541
|
+
add("image-view", err("svg-style", `the ${v} svg contains an <image>`, where, "only the breadboard (and icon) view may embed a raster image; draw this view as vectors"));
|
|
542
|
+
}
|
|
543
|
+
const href = (n.attrs["xlink:href"] ?? n.attrs.href ?? "").trim();
|
|
544
|
+
if (!href.startsWith("data:")) {
|
|
545
|
+
add(`href:${href}`, err("svg-style", `<image> href "${href.slice(0, 60)}" is not a data: URI`, where, "embed the image as a base64 data: URI; a .fzpz cannot carry external files"));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const ff = n.attrs["font-family"];
|
|
549
|
+
if (ff !== undefined) {
|
|
550
|
+
const fam = ff.trim().replace(/^['"]|['"]$/g, "");
|
|
551
|
+
if (!ALLOWED_FONTS.includes(fam)) {
|
|
552
|
+
add(`font:${fam}`, err("svg-style", `font-family "${ff}" is not allowed (on ${label}${n.text ? ` "${n.text}"` : ""})`, where, `use ${ALLOWED_FONTS.map((f) => `"${f}"`).join(" or ")}`));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return issues;
|
|
557
|
+
}
|
|
558
|
+
function checkDuplicateIds(v, nodes) {
|
|
559
|
+
const counts = new Map();
|
|
560
|
+
for (const n of nodes) {
|
|
561
|
+
const id = n.attrs.id;
|
|
562
|
+
if (id !== undefined)
|
|
563
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
564
|
+
}
|
|
565
|
+
const issues = [];
|
|
566
|
+
for (const [id, count] of counts) {
|
|
567
|
+
if (count < 2)
|
|
568
|
+
continue;
|
|
569
|
+
issues.push(err("svg-ids", `id "${id}" is used by ${count} elements in the ${v} svg`, `${v} svg`, "SVG ids must be unique; for a THT pad emit one element inside copper0 nested in copper1"));
|
|
570
|
+
}
|
|
571
|
+
return issues;
|
|
572
|
+
}
|
|
573
|
+
const COPPER_OK = new Set(["rect", "circle", "path", "polygon", "line", "g", "ellipse"]);
|
|
574
|
+
function checkCopperOnly(nodes) {
|
|
575
|
+
const issues = [];
|
|
576
|
+
const seen = new Set();
|
|
577
|
+
for (const n of nodes) {
|
|
578
|
+
if (!n.ancestors.some((a) => a.id === "copper0" || a.id === "copper1"))
|
|
579
|
+
continue;
|
|
580
|
+
if (COPPER_OK.has(n.tag) || seen.has(n.tag))
|
|
581
|
+
continue;
|
|
582
|
+
seen.add(n.tag);
|
|
583
|
+
issues.push(warn("copper-only", `<${n.tag}> inside a copper layer`, "pcb svg", "copper layers should hold only pads/traces (rect, circle, path, polygon, line, ellipse, g); move text and images to silkscreen"));
|
|
584
|
+
}
|
|
585
|
+
return issues;
|
|
586
|
+
}
|
|
587
|
+
function checkPcbLayers(m) {
|
|
588
|
+
const pcb = m.views.pcb;
|
|
589
|
+
if (!pcb)
|
|
590
|
+
return [];
|
|
591
|
+
const uses = m.connectors.some((c) => (c.views.pcb ?? []).some((p) => p.layer === "copper0"));
|
|
592
|
+
const lists = pcb.layers.includes("copper0");
|
|
593
|
+
if (uses && !lists) {
|
|
594
|
+
return [
|
|
595
|
+
err("pcb-layers", "a connector has a copper0 <p> but pcbView does not list copper0", "pcbView", 'add <layer layerId="copper0"/> to pcbView (THT parts need both copper layers)'),
|
|
596
|
+
];
|
|
597
|
+
}
|
|
598
|
+
if (!uses && lists) {
|
|
599
|
+
return [
|
|
600
|
+
warn("pcb-layers", "pcbView lists copper0 but no connector has a copper0 <p>", "pcbView", "an SMD-only part should list copper1 only"),
|
|
601
|
+
];
|
|
602
|
+
}
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
function checkSpice(m) {
|
|
606
|
+
const issues = [];
|
|
607
|
+
const declared = new Set(m.connectors.map((c) => c.id));
|
|
608
|
+
const props = new Set(m.properties.map((p) => p.name.toLowerCase()));
|
|
609
|
+
for (const line of m.spiceLines) {
|
|
610
|
+
for (const mm of line.matchAll(/\{([^{}]*)\}/g)) {
|
|
611
|
+
const tok = (mm[1] ?? "").trim();
|
|
612
|
+
const net = /^net\s+(\S+)$/.exec(tok);
|
|
613
|
+
if (net) {
|
|
614
|
+
if (!declared.has(net[1] ?? "")) {
|
|
615
|
+
issues.push(err("spice", `spice token {${tok}} references ${net[1]}, which is not a connector`, "spice", "use a connector id declared in the fzp"));
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (tok === "instanceTitle")
|
|
620
|
+
continue;
|
|
621
|
+
if (!props.has(tok.toLowerCase())) {
|
|
622
|
+
issues.push(warn("spice", `spice token {${tok}} does not match any property`, "spice", `add a property named "${tok}" or fix the token`));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return issues;
|
|
627
|
+
}
|
|
628
|
+
/** Run every structural check. Never throws: XML parse failures become issues. */
|
|
629
|
+
export function validateStructure(input) {
|
|
630
|
+
let model;
|
|
631
|
+
try {
|
|
632
|
+
model = parseFzp(input.fzp);
|
|
633
|
+
}
|
|
634
|
+
catch (e) {
|
|
635
|
+
return [
|
|
636
|
+
err("fzp-xml", `fzp is not valid XML: ${e instanceof Error ? e.message : String(e)}`, "fzp"),
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
const issues = [];
|
|
640
|
+
const svgNodes = {};
|
|
641
|
+
for (const v of VIEWS) {
|
|
642
|
+
const s = input.svgs[v];
|
|
643
|
+
if (s === undefined)
|
|
644
|
+
continue;
|
|
645
|
+
try {
|
|
646
|
+
svgNodes[v] = parseSvg(s);
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
issues.push(err("svg-xml", `${v} svg is not valid XML: ${e instanceof Error ? e.message : String(e)}`, `${v} svg`));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
issues.push(...checkModuleId(model), ...checkZipEntries(model, input), ...checkRequiredTags(model), ...checkLayerIds(model, svgNodes), ...checkConnectorRefs(model, svgNodes), ...checkConnectorNumbering(model), ...checkBuses(model));
|
|
653
|
+
for (const v of VIEWS) {
|
|
654
|
+
const nodes = svgNodes[v];
|
|
655
|
+
if (!nodes)
|
|
656
|
+
continue;
|
|
657
|
+
issues.push(...checkSvgRoot(v, nodes), ...checkSvgStyle(v, nodes), ...checkDuplicateIds(v, nodes));
|
|
658
|
+
if (v === "pcb")
|
|
659
|
+
issues.push(...checkCopperOnly(nodes));
|
|
660
|
+
}
|
|
661
|
+
issues.push(...checkPcbLayers(model), ...checkSpice(model));
|
|
662
|
+
return issues;
|
|
663
|
+
}
|
|
664
|
+
//# sourceMappingURL=structural.js.map
|