493 lines
18 KiB
Python
493 lines
18 KiB
Python
#!/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())
|