#!/usr/bin/env python3 """Return form structure context for a 1C object with base/effective/extension views.""" from __future__ import annotations import argparse import base64 import json import sys import xml.etree.ElementTree as ET from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) from get_1c_object_code_context import extension_name_from_path # noqa: E402 from resolve_1c_object import load_json, resolve_object # noqa: E402 CONTAINER_TAGS = {"ChildItems", "Attributes", "Commands", "Events"} STRUCTURE_TAGS = { "AutoCommandBar", "ButtonGroup", "Popup", "Button", "UsualGroup", "CommandBar", "InputField", "Table", "TableColumn", "Pages", "Page", "Decoration", "CheckBox", "LabelField", } def decode_arg(value: str | None, encoded: str | None) -> str | None: if encoded: return base64.b64decode(encoded).decode("utf-8") return value def local_name(tag: str) -> str: return tag.rsplit("}", 1)[-1] def child_text(element: ET.Element, name: str) -> str | None: for child in element: if local_name(child.tag) == name and child.text is not None: return child.text return None def ru_text(element: ET.Element, name: str) -> str | None: child = next((item for item in element if local_name(item.tag) == name), None) if child is None: return None for node in child.iter(): if local_name(node.tag) == "content" and node.text: return node.text return None def event_records(element: ET.Element) -> list[dict[str, Any]]: events = [] for container in element: if local_name(container.tag) != "Events": continue for event in container: if local_name(event.tag) == "Event": events.append({"name": event.attrib.get("name"), "handler": event.text}) return events def direct_named_children(element: ET.Element, container_name: str, wanted_tags: set[str] | None = None) -> list[ET.Element]: result = [] for container in element: if local_name(container.tag) != container_name: continue for child in container: if wanted_tags is None or local_name(child.tag) in wanted_tags: result.append(child) return result def element_record(element: ET.Element, *, parent: str | None, depth: int) -> dict[str, Any] | None: tag = local_name(element.tag) if tag not in STRUCTURE_TAGS: return None name = element.attrib.get("name") record = { "name": name, "id": element.attrib.get("id"), "kind": tag, "parent": parent, "depth": depth, "title": ru_text(element, "Title"), "data_path": child_text(element, "DataPath"), "command_name": child_text(element, "CommandName"), "events": event_records(element), } return {key: value for key, value in record.items() if value not in (None, [], "")} def walk_items(element: ET.Element, *, parent: str | None = None, depth: int = 0, limit: int = 500) -> list[dict[str, Any]]: records = [] for child in element: tag = local_name(child.tag) if tag in CONTAINER_TAGS: records.extend(walk_items(child, parent=parent, depth=depth, limit=limit - len(records))) continue current_parent = parent record = element_record(child, parent=parent, depth=depth) if record: records.append(record) current_parent = record.get("name") or parent if len(records) >= limit: return records[:limit] records.extend(walk_items(child, parent=current_parent, depth=depth + 1, limit=limit - len(records))) if len(records) >= limit: return records[:limit] return records def attribute_record(element: ET.Element) -> dict[str, Any]: columns = [attribute_column_record(item, parent_name=element.attrib.get("name")) for item in direct_named_children(element, "Columns", {"Column"})] record = { "name": element.attrib.get("name"), "id": element.attrib.get("id"), "title": ru_text(element, "Title"), "data_path": child_text(element, "DataPath"), "saved_data": child_text(element, "SavedData"), "view": child_text(element, "View"), "columns": columns, } value_type = child_text(element, "Type") if value_type: record["type"] = value_type return {key: value for key, value in record.items() if value not in (None, [], "")} def attribute_column_record(element: ET.Element, *, parent_name: str | None) -> dict[str, Any]: name = element.attrib.get("name") record = { "name": name, "id": element.attrib.get("id"), "title": ru_text(element, "Title"), "data_path": ".".join(item for item in (parent_name, name) if item), } value_type = child_text(element, "Type") if value_type: record["type"] = value_type return {key: value for key, value in record.items() if value not in (None, [], "")} def command_record(element: ET.Element) -> dict[str, Any]: record = { "name": element.attrib.get("name"), "id": element.attrib.get("id"), "title": ru_text(element, "Title"), "action": child_text(element, "Action"), "group": child_text(element, "Group"), "representation": child_text(element, "Representation"), } return {key: value for key, value in record.items() if value not in (None, [], "")} def form_attribute_title_index(attributes: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for attribute in attributes: name = attribute.get("name") if name: result[str(name)] = {"source": "form_attribute", "record": attribute} for column in attribute.get("columns") or []: data_path = column.get("data_path") if data_path: result[str(data_path)] = {"source": "form_attribute_column", "record": column, "parent": attribute} return result def title_edit_targets(source: str, source_name: str | None) -> dict[str, Any]: default_target = { "form_item_title": "form_item", "form_command_title": "form_command", "form_attribute_title": "form_attribute", "form_attribute_column_title": "form_attribute_column", "fallback_name": "form_item", "unresolved": "form_item", "platform_standard_command": "form_item", }.get(source, "form_item") return { "default_change_target": default_target, "one_off_change_target": "form_item", "source_name": source_name, } def resolve_item_title( item: dict[str, Any], *, commands_by_name: dict[str, dict[str, Any]], attributes_by_path: dict[str, dict[str, Any]], ) -> dict[str, Any]: explicit_title = item.get("title") if explicit_title: return { "effective_title": explicit_title, "source": "form_item_title", "source_name": item.get("name"), "edit_targets": title_edit_targets("form_item_title", item.get("name")), } command_name = str(item.get("command_name") or "") if command_name.startswith("Form.Command."): name = command_name.removeprefix("Form.Command.") command = commands_by_name.get(name) or {} title = command.get("title") or name source = "form_command_title" if command.get("title") else "fallback_name" return { "effective_title": title, "source": source, "source_name": name, "edit_targets": title_edit_targets(source, name), } if command_name.startswith("Form.StandardCommand.") or command_name.startswith("StandardCommand."): return { "effective_title": command_name.rsplit(".", 1)[-1], "source": "platform_standard_command", "source_name": command_name, "edit_targets": title_edit_targets("platform_standard_command", item.get("name")), } data_path = item.get("data_path") if data_path: attribute_hit = attributes_by_path.get(str(data_path)) if attribute_hit: record = attribute_hit["record"] title = record.get("title") or record.get("name") or str(data_path).split(".")[-1] source = f"{attribute_hit['source']}_title" if record.get("title") else "fallback_name" return { "effective_title": title, "source": source, "source_name": record.get("data_path") or record.get("name"), "edit_targets": title_edit_targets(source, record.get("data_path") or record.get("name")), } tail = str(data_path).split(".")[-1] return { "effective_title": tail, "source": "fallback_name", "source_name": data_path, "edit_targets": title_edit_targets("fallback_name", data_path), } name = item.get("name") return { "effective_title": name, "source": "fallback_name" if name else "unresolved", "source_name": name, "edit_targets": title_edit_targets("fallback_name" if name else "unresolved", name), } def attach_item_title_resolution(items: list[dict[str, Any]], attributes: list[dict[str, Any]], commands: list[dict[str, Any]]) -> list[dict[str, Any]]: commands_by_name = {str(item.get("name")): item for item in commands if item.get("name")} attributes_by_path = form_attribute_title_index(attributes) resolved = [] for item in items: copy = dict(item) resolution = resolve_item_title(copy, commands_by_name=commands_by_name, attributes_by_path=attributes_by_path) copy["effective_title"] = resolution["effective_title"] copy["title_resolution"] = {key: value for key, value in resolution.items() if key != "effective_title"} resolved.append(copy) return resolved def parse_form_xml(path: Path, *, source: str, extension_name: str | None, max_items: int) -> dict[str, Any]: root = ET.parse(path).getroot() attributes = [attribute_record(item) for item in direct_named_children(root, "Attributes", {"Attribute"})] commands = [command_record(item) for item in direct_named_children(root, "Commands", {"Command"})] items = attach_item_title_resolution(walk_items(root, limit=max_items), attributes, commands) return { "origin": {"layer": source, **({"extension": extension_name} if extension_name else {})}, "form_xml_path": str(path), "size": path.stat().st_size, "events": event_records(root), "items": items, "attributes": attributes, "commands": commands, "counts": { "items": len(items), "attributes": len(attributes), "commands": len(commands), "events": len(event_records(root)), "items_truncated": len(items) >= max_items, }, } def form_meta_record(path: Path) -> dict[str, Any]: root = ET.parse(path).getroot() form = next((item for item in root.iter() if local_name(item.tag) == "Form"), root) props = next((item for item in form if local_name(item.tag) == "Properties"), form) name = child_text(props, "Name") or path.stem return { "name": name, "synonym": ru_text(props, "Synonym"), "uuid": form.attrib.get("uuid"), "meta_xml_path": str(path), "form_xml_path": str(path.with_suffix("") / "Ext" / "Form.xml"), "module_path": str(path.with_suffix("") / "Ext" / "Form" / "Module.bsl"), } def collect_forms(owner_path: str | None, *, source: str, extension_name: str | None = None) -> list[dict[str, Any]]: if not owner_path: return [] owner_dir = Path(owner_path).with_suffix("") forms_dir = owner_dir / "Forms" if not forms_dir.is_dir(): return [] result = [] for path in sorted(forms_dir.glob("*.xml")): record = form_meta_record(path) record["origin"] = {"layer": source, **({"extension": extension_name} if extension_name else {})} result.append(record) return result def norm(value: str | None) -> str: return "".join(ch for ch in str(value or "").casefold() if not ch.isspace() and ch not in "._-") def select_form(forms: list[dict[str, Any]], wanted: str | None) -> list[dict[str, Any]]: if not wanted: return forms wanted_norm = norm(wanted) return [ form for form in forms if norm(form.get("name")) == wanted_norm or norm(form.get("synonym")) == wanted_norm or wanted_norm in norm(form.get("name")) ] def merge_effective(base_forms: list[dict[str, Any]], extension_forms: list[dict[str, Any]]) -> list[dict[str, Any]]: result = [] by_name: dict[str, dict[str, Any]] = {} for form in base_forms: copy = dict(form) copy["effective_action"] = "base" by_name[norm(copy.get("name"))] = copy result.append(copy) for form in extension_forms: copy = dict(form) key = norm(copy.get("name")) if key in by_name: copy["effective_action"] = "extended" by_name[key].setdefault("extension_overlays", []).append(copy) else: copy["effective_action"] = "added" result.append(copy) return result def attach_structure(forms: list[dict[str, Any]], *, max_items: int) -> list[dict[str, Any]]: result = [] for form in forms: copy = dict(form) form_xml = Path(str(copy.get("form_xml_path") or "")) if form_xml.is_file(): origin = copy.get("origin") or {} copy["structure"] = parse_form_xml( form_xml, source=origin.get("layer") or "base", extension_name=origin.get("extension"), max_items=max_items, ) overlays = [] for overlay in copy.get("extension_overlays") or []: overlay_copy = dict(overlay) overlay_xml = Path(str(overlay_copy.get("form_xml_path") or "")) if overlay_xml.is_file(): origin = overlay_copy.get("origin") or {} overlay_copy["structure"] = parse_form_xml( overlay_xml, source=origin.get("layer") or "extension", extension_name=origin.get("extension"), max_items=max_items, ) overlays.append(overlay_copy) if overlays: copy["extension_overlays"] = overlays result.append(copy) return result def build_context( index: dict[str, Any], *, kind: str, name: str, form: str | None, view: str, extension: str | None, max_items: int, ) -> dict[str, Any]: resolution = resolve_object(index, kind=kind, name=name, limit=100) canonical = resolution.get("canonical") if not canonical: raise SystemExit(f"Object not found: {kind}.{name}") base_forms = collect_forms(canonical.get("path"), source="base") extension_forms = [] for overlay in resolution.get("extension_overlays") or []: overlay_extension = extension_name_from_path(overlay.get("path")) or overlay.get("extension_name") extension_forms.extend(collect_forms(overlay.get("path"), source="extension", extension_name=overlay_extension)) if view == "base": forms = [{**item, "effective_action": "base"} for item in base_forms] elif view == "effective": forms = merge_effective(base_forms, extension_forms) elif view == "extension": if not extension: raise SystemExit("Use --extension with --view extension.") forms = [{**item, "effective_action": "extended_or_added"} for item in extension_forms if (item.get("origin") or {}).get("extension") == extension] else: raise SystemExit(f"Unsupported view: {view}") selected = attach_structure(select_form(forms, form), max_items=max_items) return { "schema": "onec_form_context.v1", "view": view, "extension": extension if view == "extension" else None, "object": { "kind": canonical.get("kind"), "name": canonical.get("name"), "synonym": canonical.get("synonym"), "uuid": canonical.get("guid"), }, "query": {"form": form, "max_items": max_items}, "forms": selected, "counts": { "forms": len(selected), "base_forms": len(base_forms), "extension_forms": len(extension_forms), }, } def main() -> int: parser = argparse.ArgumentParser(description="Get 1C form context.") parser.add_argument("--index", type=Path, required=True) parser.add_argument("--kind") parser.add_argument("--name") parser.add_argument("--kind-b64") parser.add_argument("--name-b64") parser.add_argument("--form") parser.add_argument("--form-b64") parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective") parser.add_argument("--extension") parser.add_argument("--max-items", type=int, default=500) parser.add_argument("--output", type=Path) args = parser.parse_args() kind = decode_arg(args.kind, args.kind_b64) name = decode_arg(args.name, args.name_b64) form = decode_arg(args.form, args.form_b64) if not kind or not name: raise SystemExit("Use --kind/--name or --kind-b64/--name-b64.") result = build_context( load_json(args.index), kind=kind, name=name, form=form, view=args.view, extension=args.extension, max_items=args.max_items, ) text = json.dumps(result, ensure_ascii=False, indent=2) + "\n" if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(text, encoding="utf-8") print(json.dumps({"output": str(args.output), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False)) else: print(text) return 0 if __name__ == "__main__": raise SystemExit(main())