Extract WPF/XAML structure, bindings, x:Class, and event handler references.
(path: Path)
| 14778 | |
| 14779 | |
| 14780 | def extract_xaml(path: Path) -> dict: |
| 14781 | """Extract WPF/XAML structure, bindings, x:Class, and event handler references.""" |
| 14782 | import xml.etree.ElementTree as ET |
| 14783 | |
| 14784 | try: |
| 14785 | src = path.read_bytes() |
| 14786 | except OSError: |
| 14787 | return {"nodes": [], "edges": [], "error": f"cannot read {path}"} |
| 14788 | |
| 14789 | if len(src) > _PROJECT_XML_MAX_BYTES: |
| 14790 | return {"nodes": [], "edges": [], "error": "xaml file too large"} |
| 14791 | if not _project_xml_is_safe(src): |
| 14792 | return {"nodes": [], "edges": [], |
| 14793 | "error": "refusing XML with DOCTYPE/ENTITY declaration"} |
| 14794 | |
| 14795 | try: |
| 14796 | tree = ET.fromstring(src) |
| 14797 | except ET.ParseError as e: |
| 14798 | return {"nodes": [], "edges": [], "error": f"XML parse error: {e}"} |
| 14799 | |
| 14800 | text = src.decode("utf-8", errors="replace") |
| 14801 | lines = text.splitlines() |
| 14802 | str_path = str(path) |
| 14803 | stem = _file_stem(path) |
| 14804 | file_nid = _make_id(str(path)) |
| 14805 | root_type = _xml_local_name(tree.tag) |
| 14806 | root_nid = _make_id(stem, root_type) |
| 14807 | nodes: list[dict] = [] |
| 14808 | edges: list[dict] = [] |
| 14809 | seen_ids: set[str] = set() |
| 14810 | seen_edges: set[tuple[str, str, str, str | None]] = set() |
| 14811 | |
| 14812 | def line_for(value: str | None) -> int: |
| 14813 | if value: |
| 14814 | for idx, line in enumerate(lines, 1): |
| 14815 | if value in line: |
| 14816 | return idx |
| 14817 | return 1 |
| 14818 | |
| 14819 | def add_node( |
| 14820 | nid: str, |
| 14821 | label: str, |
| 14822 | line: int | None, |
| 14823 | *, |
| 14824 | file_type: str = "code", |
| 14825 | source_file: str = str_path, |
| 14826 | ) -> None: |
| 14827 | if nid in seen_ids: |
| 14828 | return |
| 14829 | seen_ids.add(nid) |
| 14830 | nodes.append({ |
| 14831 | "id": nid, "label": label, "file_type": file_type, |
| 14832 | "source_file": source_file, |
| 14833 | "source_location": f"L{line}" if line else None, |
| 14834 | }) |
| 14835 | |
| 14836 | def add_existing_node(node: dict | None) -> None: |
| 14837 | if not node: |