"""Document-level helpers for a Sedona app: the packed 'meta' property and component paths. Pure XML and bit work, deliberately free of Tk so the rules stay testable headless. """ import copy import xml.etree.ElementTree as ET def snapshot_children(element): """Copies an element's children and leading text, so it can be put back wholesale. Used for the block, which is short enough that copying it whole is easier to trust than tracking which attribute moved and which entry was inserted where. """ return [copy.deepcopy(child) for child in element], element.text def restore_children(element, snapshot): """Puts back what snapshot_children captured, whitespace and all.""" children, text = snapshot for child in list(element): element.remove(child) element.text = text for child in children: element.append(copy.deepcopy(child)) def set_prop_value(element, name, value): """Writes a component's , creating it in the block the other props sit in. A .sax keeps every ahead of the child elements, and ET.SubElement would append past them, so a new prop is inserted after the last existing one instead. That is cosmetic to a parser and not to a human reading the diff. """ prop = element.find(f"prop[@name='{name}']") if prop is not None: prop.set("val", value) return prop prop = ET.Element("prop", name=name, val=value) last_prop_index = -1 for index, child in enumerate(element): if child.tag == "prop": last_prop_index = index element.insert(last_prop_index + 1, prop) return prop def remove_prop(element, name): """Drops a component's , for undoing a value that the file never carried.""" prop = element.find(f"prop[@name='{name}']") if prop is not None: element.remove(prop) def read_meta_property(meta_str): """Parses 32-bit meta property integer fields via structured byte extraction rules.""" if meta_str is None or not meta_str: return 0, 0, 0, True, False, False, False, 0 try: if meta_str.startswith("0x") or meta_str.startswith("0X"): meta_val = int(meta_str, 16) else: meta_val = int(meta_str) raw_x = (meta_val >> 24) & 0xFF raw_y = (meta_val >> 16) & 0xFF reserved = (meta_val >> 8) & 0xFF user_group_byte = meta_val & 0xFF group1 = bool(user_group_byte & 0x01) group2 = bool(user_group_byte & 0x02) group3 = bool(user_group_byte & 0x04) group4 = bool(user_group_byte & 0x08) user_group_reserved = (user_group_byte >> 4) & 0x0F return raw_x, raw_y, reserved, group1, group2, group3, group4, user_group_reserved except Exception: return 0, 0, 0, True, False, False, False, 0 def update_meta_property(element, meta_str, raw_x, raw_y): """Re-encodes coordinates back into meta, completely preserving existing lower and consecutive bytes.""" _, _, reserved, g1, g2, g3, g4, ug_res = read_meta_property(meta_str) user_group_byte = 0 if g1: user_group_byte |= 0x01 if g2: user_group_byte |= 0x02 if g3: user_group_byte |= 0x04 if g4: user_group_byte |= 0x08 user_group_byte |= (ug_res & 0x0F) << 4 new_val = ( ((raw_x & 0xFF) << 24) | ((raw_y & 0xFF) << 16) | ((reserved & 0xFF) << 8) | (user_group_byte & 0xFF) ) meta_prop = element.find("prop[@name='meta']") if meta_prop is None: meta_prop = ET.SubElement(element, "prop", name="meta") meta_prop.set("val", str(new_val)) def update_meta_groups(element, meta_str, groups): """Rewrites only meta's four user-group bits, leaving the packed position alone. The mirror of update_meta_property, which rewrites only x and y. Both exist because meta is one integer carrying two unrelated things, and an editor that touches one of them must not disturb the other. """ raw_x, raw_y, reserved, _, _, _, _, ug_res = read_meta_property(meta_str) user_group_byte = 0 for bit, on in enumerate(groups[:4]): if on: user_group_byte |= (1 << bit) user_group_byte |= (ug_res & 0x0F) << 4 new_val = ( ((raw_x & 0xFF) << 24) | ((raw_y & 0xFF) << 16) | ((reserved & 0xFF) << 8) | (user_group_byte & 0xFF) ) meta_prop = element.find("prop[@name='meta']") if meta_prop is None: meta_prop = ET.SubElement(element, "prop", name="meta") meta_prop.set("val", str(new_val)) def find_parent_element(root_elem, child_to_find): """Traverses the XML DOM tree recursively to find the parent element of a given element.""" for parent in root_elem.iter(): if child_to_find in list(parent): return parent return None def find_component_by_path(root_elem, path): """The component an absolute path names, or None if the document has no such node. The inverse of get_component_path, and the way a 's from/to reference is turned back into the component it points at. Paths carry no entry for , so resolution starts among its children. """ if root_elem is None or not path: return None names = [part for part in path.split("/") if part] if not names: return None node = root_elem.find("app") if node is None: node = root_elem for name in names: node = next( (child for child in node if child.tag == "comp" and child.get("name") == name), None, ) if node is None: return None return node def get_component_path(element, root_elem): """Computes the absolute path of a component within the application tree hierarchy.""" path_parts = [] curr = element while curr is not None and curr.tag == "comp": path_parts.append(curr.get("name", "")) curr = find_parent_element(root_elem, curr) path_parts.reverse() return "/" + "/".join(path_parts)