"""Mechanical profiles for decoded 1C form payloads (root marker 4).""" from __future__ import annotations import re from typing import Any from .child_records import collect_evidence, declared_child_records from .payload import GUID_RE, collect_strings, root_signature, scalar from .structured_metadata import get_by_path BSL_ROUTINE_RE = re.compile(r"(?im)^\s*(?:&[^\r\n]+\s*)*(?:Асинх\s+)?(Процедура|Функция)\s+([A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*)") BSL_IDENTIFIER_RE = re.compile(r"^[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*$") BSL_PATH_RE = re.compile(r"^[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*(?:\.[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*)*$") FORM_EVENT_NAMES = { "01d80ddd-dce5-4db3-beb5-f63c97cb05b9": "OnEditEnd", "047d4d09-961c-4bdc-8519-eef10674c35b": "AfterWrite", "11707a99-4eb9-4373-bc8c-84891483a034": "Click", "1282f000-23b6-4887-87f4-9e8e79db3d32": "Selection", "14256303-d2b7-4a58-bfab-e77493d10a59": "EditTextChange", "178a97c4-0ffe-4fcc-93e6-505369939da5": "AutoComplete", "1960479b-4d89-4eba-8b39-0aa802020558": "StartChoice", "213d1900-dcad-4616-9f20-3f077156a40f": "AfterWriteAtServer", "2391e7b8-7235-45d7-ab7e-6ff3dc086396": "BeforeAddRow", "2988b2a5-c887-4928-94ae-5d0c9c31e999": "DetailProcessing", "2ccfdec5-583d-4eca-8319-e55de492665a": "BeforeDeleteRow", "3699f6a3-9a2a-4c82-a775-6ff4824a08ca": "NotificationProcessing", "390d5e4b-e732-4c88-8748-9e211a416984": "OnReadAtServer", "1952a54f-35ad-4928-902f-df212ab38ca3": "OnSaveDataInSettingsAtServer", "3c3da18f-fc18-4f77-8c2d-96c25bec40a5": "Selection", "3ccc650e-f631-4cae-8e33-3eaac610b5f9": "OnOpen", "526c501f-ed3f-4db4-8731-fd0324707501": "OnCurrentPageChange", "509eca20-d6e4-4fef-a0f8-3a6b44c64178": "URLProcessing", "60edb81d-887b-478e-94ee-7fef2b13393d": "OnActivateRow", "650da4af-3233-4ce0-a1ae-23f87a226eee": "DetailProcessing", "8a5894c9-d2ff-4c1d-b433-89cc352bbfbc": "BeforeWrite", "8f42e083-be92-4102-b1f0-fa58452c1a63": "BeforeWriteAtServer", "93dfba16-26db-46f8-acb5-4f92f50c855f": "NavigationProcessing", "9f2e5ddb-3492-4f5d-8f0d-416b8d1d5c5b": "OnCreateAtServer", "97365900-eadf-4dfd-a9aa-fbb9ecabd079": "OnGetDataAtServer", "9874537f-454c-40ae-83e9-3b9cefbc6d08": "Click", "ab930362-ff94-4dcb-ad16-188805d23e3c": "BeforeRowChange", "aeba313d-c467-44b3-b4a2-956340932c8f": "Creating", "ac5a9c5a-5f1d-4fc5-b88c-a187038c16d1": "Opening", "b3c10170-c5ff-4cba-b537-679e1c872b45": "OnStartEdit", "bf0ac0e1-bcbb-4dfe-8fc4-0b1923b461a6": "BeforeWriteAtServer", "b50dc41b-c15a-4ebe-a17f-d01e51c47de6": "Clearing", "c331eb1b-d32b-4533-844c-1276600b64e3": "TextEditEnd", "ca21cd18-35b2-4281-b5c8-016ecc8da8ac": "OnClose", "d710ea07-5c96-4c43-ab6e-e138d3653780": "URLProcessing", "de65638d-a806-4a76-bc10-f62bbc86e0e7": "AfterDeleteRow", "eba5f295-c611-4dd9-84b5-22911ad60c53": "Click", "e773807c-0c0c-4689-a093-231ddcd6409f": "BeforeLoadDataFromSettingsAtServer", "e73d6384-49d2-4885-a752-a674d6ff7742": "FillCheckProcessingAtServer", "70636369-514c-4662-977e-1c3976c9756c": "Tuning", "f228b12f-d892-4925-b338-695617357b32": "OnActivateCell", "f72043b8-2d79-414e-bc4e-3972fe9dbca1": "ChoiceProcessing", "fe115cc8-9e33-4684-a166-bd5136fe7a9f": "OnChange", } MARKER_NAMES = { "9": "AttributeOrCommand", "12": "ExtendedTooltip", "22": "ContainerItem", "31": "CommandBarButton", "34": "Button", "35": "InputField", "37": "InputField", "55": "DynamicListTable", } FORM_ITEM_TYPE_NAMES = { "0": "Командная панель", "1": "Подменю", "2": "Группа колонок", "3": "Страницы", "4": "Страница", "5": "Группа", "6": "Группа кнопок", "8": "Контекстное меню", "9": "Командная панель", "12": "Расширенная подсказка", "31": "Кнопка командной панели", "34": "Кнопка", "48": "Поле формы", "55": "Динамический список", "73": "Таблица формы", } FORM_TABLE_ADDITION_TYPE_NAMES = { "0": "SearchStringAddition", "1": "ViewStatusAddition", "2": "SearchControlAddition", } FORM_FIELD_SUBTYPE_NAMES = { "1": "Поле надписи", "2": "Поле ввода", "3": "Поле флажка", "4": "PictureField", "5": "Поле переключателя", "6": "Поле табличного документа", "11": "ChartField", "14": "GraphicalSchemaField", } FORM_DECORATION_TYPE_NAMES = { "0": "Декорация надписи", "1": "Декорация картинки", } FORM_LOCAL_COMMAND_GROUP_GUID = "409b9a53-7f7e-4178-86c1-33176c7c7a7a" FORM_STANDARD_COMMANDS = { ("198ea630-fda2-4cda-8a23-f999f4c67ee6", "0"): "Form.StandardCommand.CustomizeForm", ("39bb0fe9-771d-4dd5-8a6e-2d16984523af", "0"): "Form.StandardCommand.Help", ("fe558fde-99b3-45d0-a060-9fc2905309f6", "0"): "Form.StandardCommand.Write", ("1f317795-c420-4a30-b594-c492abc55f7a", "0"): "Form.StandardCommand.Reread", ("68baa1bc-edd1-4d9b-ad80-1d53fb8a7988", "0"): "Form.StandardCommand.Copy", ("827b541d-30c1-4f06-aecf-92aa496a0835", "0"): "Form.StandardCommand.SetDeletionMark", ("3a17e914-ec6a-4280-b4df-78914f40522b", "0"): "Form.StandardCommand.ShowInList", ("174e58ce-82ad-4787-b956-9367937f7971", "0"): "Form.StandardCommand.ChangeHistory", ("6886601d-276c-4d3f-af0a-05c586025608", "0"): "Form.StandardCommand.Change", ("bdefa701-6685-453e-a02a-3683d0cc16d3", "0"): "Form.StandardCommand.Find", ("96e0bc70-f8ff-4732-8119-060923203629", "0"): "Form.StandardCommand.CancelSearch", } FORM_GRAPHICAL_SCHEMA_STANDARD_COMMANDS = { ("e2d6f793-b786-4640-a91b-8d77f73860f1", "3"): "Print", ("1d13f9a3-402a-46cb-9c68-1709356840f2", "3"): "Preview", ("01db2225-b62d-4112-a4b6-d39d627bf79f", "3"): "PageSetup", } FORM_TABLE_STANDARD_COMMANDS = { "b0016a68-ec64-4e6d-b905-c71fd62efc4c": "Add", "0ae4bea5-23be-42a7-b69e-97b11b29c453": "Copy", "b41f5bbc-ba5d-4888-8cd1-db246a371418": "Change", "8d772f97-c0ef-47c0-9cb0-efea28c61341": "Delete", "9ef79140-3de6-436a-8dda-610bb963f5db": "EndEdit", "daa306cd-a78a-4e74-a14c-739daba624cb": "SetDateInterval", "c0519548-2a9a-44de-a25e-faf01e089d4d": "Find", "44ad3ec9-f3c2-4913-9224-5f9fb6418743": "CancelSearch", "88078230-1f6b-415f-99e4-ad2ff73810cf": "CopyToClipboard", "37740564-9e86-44a0-bea9-3f485a5a3f91": "MoveUp", "fa51b106-eae6-44c7-8054-76cbb3100603": "MoveDown", "2bbe4e12-06d2-409b-a972-eea585125d83": "SortListAsc", "58b2a785-23f6-4b0e-a324-9a1323285595": "SortListDesc", "49602716-fea6-497f-8047-726404038857": "OutputList", } FORM_OBJECT_COMMANDS = { ("0fa77ef7-a836-4459-9bca-6010d0bdfc7f", "0"): "Выполнено", ("dcff004c-1b61-4a19-b977-ea21bf688614", "0"): "Перенаправить", } FORM_STANDARD_DATA_FIELDS = { "-2": "Code", "-3": "Description", "-4": "Parent", "-5": "Ref", } FORM_PUBLIC_DATA_FIELD_NAMES = { "Код": "Code", "Номер": "Number", "Наименование": "Description", "Родитель": "Parent", "НомерСтроки": "LineNumber", } FORM_ITEM_MARKERS = {"6", "12", "22", "31", "34", "35", "37", "48", "55", "73"} FORM_ITEM_PARAMETER_ROLES = { "6": { 0: "Маркер дополнения таблицы", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Вид дополнения", 6: "Имя", }, "12": { 0: "Маркер расширенной подсказки", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 6: "Имя", }, "22": { 0: "Маркер элемента", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Вид элемента", 6: "Имя", 7: "Заголовок", }, "31": { 0: "Маркер кнопки командной панели", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Имя", 6: "Заголовок", }, "34": { 0: "Маркер кнопки", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Вид кнопки", 6: "Имя", }, "35": { 0: "Маркер поля ввода", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Вид элемента", 6: "Имя", 9: "Заголовок", 11: "Путь к данным", }, "37": { 0: "Маркер поля ввода", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Вид элемента", 6: "Имя", 9: "Заголовок", 11: "Путь к данным", }, "48": { 0: "Маркер поля формы", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Вид элемента", 6: "Имя", 12: "Путь к данным", }, "55": { 0: "Маркер динамического списка", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Имя", 11: "Путь к данным", }, "73": { 0: "Маркер таблицы формы", 1: "Идентификатор", 2: "Использование", 3: "Подчинение", 4: "Группа", 5: "Имя", 12: "Путь к данным", }, } SECTION_RECORD_PARAMETER_ROLES = { 0: "Маркер записи", 1: "Идентификатор", 2: "Имя", 3: "Заголовок", 6: "Имя", } FORM_ITEM_SEMANTIC_PROPERTIES = { "6": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Вид"), 6: ("Основные", "Имя"), }, "12": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 6: ("Основные", "Имя"), }, "22": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Вид"), 6: ("Основные", "Имя"), 7: ("Основные", "Заголовок"), }, "31": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Имя"), 6: ("Основные", "Заголовок"), }, "34": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Вид"), 6: ("Основные", "Имя"), }, "35": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Вид"), 6: ("Основные", "Имя"), 9: ("Основные", "Заголовок"), }, "37": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Вид"), 6: ("Основные", "Имя"), 9: ("Основные", "Заголовок"), }, "48": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Вид"), 6: ("Основные", "Имя"), }, "55": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Имя"), }, "73": { 1: ("Основные", "Идентификатор"), 2: ("Иерархия", "Использование"), 3: ("Иерархия", "Подчинение"), 4: ("Иерархия", "Группа"), 5: ("Основные", "Имя"), }, } SECTION_RECORD_SEMANTIC_PROPERTIES = { 1: ("Основные", "Идентификатор"), 2: ("Основные", "Имя"), 3: ("Основные", "Заголовок"), 6: ("Основные", "Имя"), } def children(node: Any) -> list[Any]: if isinstance(node, dict) and node.get("type") in {"list", "sequence"}: return node.get("items") or [] return [] def child_at(node: Any, index: int) -> Any: items = children(node) return items[index] if 0 <= index < len(items) else None def atoms(node: Any, *, limit: int = 100) -> list[str]: result: list[str] = [] def walk(value: Any) -> None: if len(result) >= limit: return text = scalar(value) if text: result.append(text) return for child in children(value): walk(child) walk(node) return result def guids(node: Any, *, limit: int = 100) -> list[str]: return [value.lower() for value in atoms(node, limit=limit * 4) if GUID_RE.fullmatch(value)][:limit] def child_scalar(node: Any, index: int) -> str | None: items = children(node) if index < 0 or index >= len(items): return None return scalar(items[index]) def direct_scalar(node: Any) -> str | None: if isinstance(node, dict) and node.get("type") in {"atom", "string", "number", "guid", "base64"}: value = node.get("value") return "" if value is None else str(value) return None def child_direct_scalar(node: Any, index: int) -> str | None: items = children(node) if index < 0 or index >= len(items): return None return direct_scalar(items[index]) def child_direct_scalar_from_end(node: Any, offset: int) -> tuple[str | None, int | None]: items = children(node) index = len(items) + offset if offset >= 0 or index < 0 or index >= len(items): return None, None return direct_scalar(items[index]), index def scalar_kind(value: str | None) -> str: if value is None: return "empty" if GUID_RE.fullmatch(value): return "guid" if value in {"0", "1"}: return "boolean_or_number" if re.fullmatch(r"-?\d+", value): return "number" if re.fullmatch(r"-?\d+(?:\.\d+)?", value): return "number" return "string" def path_join(path: str, index: int) -> str: return f"{path}.{index}" if path else str(index) def node_scalar_values(node: Any, *, limit: int = 40) -> list[dict[str, Any]]: values: list[dict[str, Any]] = [] def walk(value: Any, path: str) -> None: if len(values) >= limit: return text = scalar(value) if text is not None: values.append({"value": text, "kind": scalar_kind(text), "position": {"indices": [int(part) for part in path.split(".") if part.isdigit()]}}) return for index, child in enumerate(children(value)): walk(child, path_join(path, index)) walk(node, "") return values def node_scalar_entries(node: Any, base_path: str = "", *, limit: int = 20000) -> list[dict[str, Any]]: result: list[dict[str, Any]] = [] def walk(value: Any, path: str) -> None: if len(result) >= limit: return text = direct_scalar(value) if text is not None: result.append({"value": text, "path": path, "kind": scalar_kind(text)}) return for index, child in enumerate(children(value)): walk(child, path_join(path, index)) walk(node, base_path) return result def direct_parameters( node: Any, base_path: str, *, roles: dict[int, str] | None = None, limit: int = 200, ) -> list[dict[str, Any]]: result: list[dict[str, Any]] = [] for index, child in enumerate(children(node)[:limit]): item_path = path_join(base_path, index) text = scalar(child) entry: dict[str, Any] = { "index": index, "presentation": (roles or {}).get(index) or f"Параметр {index}", "position": {"indices": [int(part) for part in item_path.split(".") if part.isdigit()]}, } if text is not None: entry["value"] = text entry["value_kind"] = scalar_kind(text) else: child_items = children(child) evidence = collect_evidence(child) localized = localized_text(child, item_path, max_depth=3) entry.update( { "kind": "group", "items": len(child_items), "strings": sorted(evidence["strings"])[:20], "guids": sorted(evidence["guids"])[:20], "values_sample": node_scalar_values(child, limit=20), } ) if localized: entry["localized_text"] = {"lang": localized.get("lang"), "text": localized.get("value")} result.append(entry) return result def parameter_value(parameters: list[dict[str, Any]], index: int) -> Any: for parameter in parameters: if parameter.get("index") == index: return parameter.get("value") return None def semantic_property(name: str, value: Any, *, index: int | None = None, source: str = "form_payload") -> dict[str, Any]: result = { "name": name, "value": value, "source": source, "status": "ok" if value is not None and value != "" else "empty", } if index is not None: result["parameter_index"] = index return result def item_type_name(marker: str | None, type_code: str | None) -> str | None: marker_text = str(marker or "") type_text = str(type_code or "") if marker_text == "6": return FORM_TABLE_ADDITION_TYPE_NAMES.get(type_text, type_text or None) if marker_text in {"35", "37", "48"}: return FORM_FIELD_SUBTYPE_NAMES.get(type_text, "Поле") if marker_text == "22": return FORM_ITEM_TYPE_NAMES.get(type_text, type_text or None) return FORM_ITEM_TYPE_NAMES.get(marker_text, type_text or marker_text or None) def form_item_public_type_name(marker: str | None, type_code: str | None, name: str | None) -> str | None: """Resolve form element type using payload shape plus stable XML-confirmed naming rules.""" marker_text = str(marker or "") name_text = str(name or "") if marker_text == "12": if name_text.endswith(("РасширеннаяПодсказка", "ExtendedTooltip")): return "Расширенная подсказка" return FORM_DECORATION_TYPE_NAMES.get(str(type_code or ""), "Декорация") return item_type_name(marker, type_code) def bool_presentation(value: str | None) -> bool | None: if value == "1": return True if value == "0": return False return None def auto_bool_presentation(value: str | None) -> str | bool | None: if value == "1": return True if value == "0": return False if value == "2": return "Авто" return None def title_location_presentation(value: str | None) -> str | None: return { "0": "None", "1": "Auto", "2": "Left", "3": "Top", "4": "Right", }.get(str(value) if value is not None else "") def horizontal_align_presentation(value: str | None) -> str | None: return { "0": "Left", "1": "Center", "2": "Right", "3": "Auto", }.get(str(value) if value is not None else "") def vertical_align_presentation(value: str | None) -> str | None: return { "0": "Top", "1": "Center", "2": "Bottom", "3": "Auto", }.get(str(value) if value is not None else "") def button_importance_presentation(value: str | None) -> str | None: return { "0": "Supplementary", "1": "Main", }.get(str(value) if value is not None else "") def input_edit_mode_presentation(value: str | None) -> str | None: return { "0": "Directly", "1": "Auto", "2": "EnterOnInput", }.get(str(value) if value is not None else "") def tooltip_representation_presentation(value: str | None) -> str | None: return { "0": "Auto", "3": "Button", }.get(str(value) if value is not None else "") def choice_folders_and_items_presentation(value: str | None) -> str | None: return { "0": "Items", "3": "Auto", }.get(str(value) if value is not None else "") def edit_text_update_presentation(value: str | None) -> str | None: return { "0": "Auto", "2": "OnValueChange", }.get(str(value) if value is not None else "") def choice_button_representation_presentation(value: str | None) -> str | None: return { "0": "Auto", "2": "ShowInDropListAndInInputField", "3": "ShowInInputField", }.get(str(value) if value is not None else "") def choice_history_on_input_presentation(value: str | None) -> str | None: return { "0": "Auto", "1": "DontUse", }.get(str(value) if value is not None else "") def shortcut_presentation(node: Any) -> str | None: """Decode the confirmed managed-form shortcut tuple ``{0,key,modifiers}``.""" if child_direct_scalar(node, 0) != "0": return None key_code = child_direct_scalar(node, 1) modifiers = child_direct_scalar(node, 2) if not key_code or modifiers is None: return None try: code = int(key_code) modifier_mask = int(modifiers) except ValueError: return None if 112 <= code <= 123: key_name = f"F{code - 111}" elif 32 <= code <= 126: key_name = chr(code).upper() else: return None modifier_names = [] if modifier_mask & 8: modifier_names.append("Ctrl") if modifier_mask & 16: modifier_names.append("Alt") if modifier_mask & 4: modifier_names.append("Shift") return "+".join([*modifier_names, key_name]) def choice_parameter_links_presentation(node: Any) -> dict[str, Any] | None: """Decode the stable public part of an input field ChoiceParameterLinks node. Marker 5007 is followed by the declared link count. Link data paths use internal form attribute references, so only the public link names are exposed here until those references are resolved independently. """ if child_direct_scalar(node, 0) != "5007": return None try: count = int(child_direct_scalar(node, 1) or "0") except ValueError: return None if count <= 0: return None names: list[str] = [] for item in children(node)[2:]: value = direct_scalar(item) if not value or not BSL_PATH_RE.fullmatch(value): continue names.append(value) if len(names) == count: break if len(names) != count: return None return { "count": count, "links": [{"name": name} for name in names], } def choice_list_presentation(node: Any) -> dict[str, Any] | None: """Decode confirmed numeric ChoiceList items from input-field payloads.""" if child_direct_scalar(node, 0) != "3": return None try: count = int(child_direct_scalar(node, 1) or "0") except ValueError: return None if count <= 0: return None result: list[dict[str, Any]] = [] node_items = children(node) for item_index in range(count): encoded_item = node_items[3 + item_index * 2] if 3 + item_index * 2 < len(node_items) else None if child_direct_scalar(encoded_item, 0) != "#": return None payload = child_at(encoded_item, 2) encoded_value = child_at(payload, 2) value_kind = child_direct_scalar(encoded_value, 0) localized = localized_text(encoded_item, "", max_depth=8) presentation = str((localized or {}).get("value") or "") if value_kind == "N": scalar_value = child_direct_scalar(encoded_value, 1) if scalar_value is None: return None value: dict[str, Any] = {"kind": "Number", "value": scalar_value} elif value_kind == "U": type_guid = child_direct_scalar(payload, 3) value_guid = child_direct_scalar(payload, 4) if not type_guid or not value_guid or not GUID_RE.fullmatch(type_guid) or not GUID_RE.fullmatch(value_guid): return None value = { "kind": "EnumValue", "type_guid": type_guid.lower(), "value_guid": value_guid.lower(), "status": "identity_pending", } else: return None result.append( { "presentation": presentation, "value": value, } ) return {"count": count, "items": result} def type_link_reference(node: Any) -> dict[str, Any] | None: if child_direct_scalar(node, 0) != "3" or child_direct_scalar(node, 1) != "2": return None field_reference = children(node)[3] if len(children(node)) > 3 else None field_ids = [value for value in atoms(field_reference, limit=8) if re.fullmatch(r"\d+", value or "")] if not field_ids: return None link_item = child_direct_scalar(node, 4) return { "field_id": field_ids[-1], "link_item": int(link_item) if re.fullmatch(r"\d+", str(link_item or "")) else 0, } def style_value_presentation(node: Any) -> str | dict[str, Any] | None: if child_direct_scalar(node, 0) != "3": return None variant = child_direct_scalar(node, 1) payload = children(node)[2] if len(children(node)) > 2 else None code = child_direct_scalar(payload, 0) if variant == "4" and code == "0": return "Авто" if variant == "1" and code == "18": return "win:ButtonText" if variant == "2" and code == "27": return "web:DarkGreen" if variant != "3": return None standard = { "-21": "style:ButtonTextColor", "-22": "style:BorderColor", "-23": "style:ToolTipBackColor", "-35": "style:TableHeaderBackColor", "-1": "style:FormBackColor", }.get(str(code or "")) if standard: return standard guid = child_direct_scalar(payload, 1) if str(guid or "").lower() == "ad87bd29-0ad1-4da4-ac62-38e714e0cb9f": return "style:ПоясняющийТекст" if guid and GUID_RE.fullmatch(guid): return { "kind": "StyleItem", "guid": guid.lower(), "status": "identity_pending", } return None def font_value_presentation(node: Any) -> str | dict[str, Any] | None: if child_direct_scalar(node, 0) != "7": return None variant = child_direct_scalar(node, 1) if variant == "3": return "Авто" if variant != "2": return None reference = children(node)[3] if len(children(node)) > 3 else None code = child_direct_scalar(reference, 0) standard = { "-31": "style:NormalTextFont", "-32": "style:LargeTextFont", }.get(str(code or "")) if standard: return standard guid = child_direct_scalar(reference, 1) if code == "0" and guid and GUID_RE.fullmatch(guid): return { "kind": "StyleItem", "guid": guid.lower(), "status": "identity_pending", } return None def auto_enum_presentation(value: str | None) -> str | None: return {"3": "Авто"}.get(str(value) if value is not None else "") def button_representation_presentation(value: str | None) -> str | None: return { "0": "Text", "1": "Picture", "2": "PictureAndText", "3": "Авто", }.get(str(value) if value is not None else "") def command_bar_location_presentation(value: str | None) -> str | None: return { "0": "Авто", "1": "В командной панели", "2": "В дополнительном подменю", }.get(str(value) if value is not None else "") def add_grouped_property(groups: dict[str, list[dict[str, Any]]], group: str, prop: dict[str, Any]) -> None: groups.setdefault(group, []).append(prop) def mark_semantic_parameter_mapped(semantic: dict[str, Any], index: int) -> None: unmapped = semantic.get("unmapped_parameters") coverage = semantic.get("coverage") if not isinstance(unmapped, list) or not isinstance(coverage, dict): return before = len(unmapped) semantic["unmapped_parameters"] = [item for item in unmapped if item.get("index") != index] if len(semantic["unmapped_parameters"]) == before: return coverage["mapped"] = int(coverage.get("mapped") or 0) + (before - len(semantic["unmapped_parameters"])) coverage["unmapped"] = max(0, int(coverage.get("unmapped") or 0) - (before - len(semantic["unmapped_parameters"]))) coverage["status"] = "partial" if coverage["unmapped"] else "ok" def semantic_coverage(parameters: list[dict[str, Any]], mapped_indexes: set[int]) -> dict[str, Any]: available = [item for item in parameters if item.get("index") is not None] unmapped = [item for item in available if item.get("index") not in mapped_indexes] return { "mapped": len(available) - len(unmapped), "unmapped": len(unmapped), "total": len(available), "status": "partial" if unmapped else "ok", } def semantic_unmapped_parameters(parameters: list[dict[str, Any]], mapped_indexes: set[int]) -> list[dict[str, Any]]: result = [] for parameter in parameters: if parameter.get("index") in mapped_indexes: continue public = { "index": parameter.get("index"), "presentation": parameter.get("presentation"), "value": parameter.get("value"), "value_kind": parameter.get("value_kind"), "kind": parameter.get("kind"), "items": parameter.get("items"), "strings": parameter.get("strings"), "guids": parameter.get("guids"), "localized_text": parameter.get("localized_text"), } result.append({key: value for key, value in public.items() if value is not None and value != [] and value != {}}) return result def public_semantic(semantic: dict[str, Any], *, include_diagnostics: bool) -> dict[str, Any]: if include_diagnostics: return semantic groups: dict[str, list[dict[str, Any]]] = {} for group, properties in (semantic.get("groups") or {}).items(): groups[group] = [ {key: prop.get(key) for key in ("name", "value", "status") if key in prop} for prop in properties if isinstance(prop, dict) ] return {"groups": groups} def public_rows_semantics(rows: list[dict[str, Any]], *, include_diagnostics: bool) -> None: if include_diagnostics: return for row in rows: semantic = row.get("semantic") if isinstance(semantic, dict): row["semantic"] = public_semantic(semantic, include_diagnostics=False) def add_derived_semantic_property(row: dict[str, Any], group: str, name: str, value: Any, *, source: str) -> None: if value is None or value == "": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) for prop in groups.get(group, []): if isinstance(prop, dict) and prop.get("name") == name: return add_grouped_property(groups, group, semantic_property(name, value, source=source)) def form_item_semantic_properties(row: dict[str, Any], parameters: list[dict[str, Any]]) -> dict[str, Any]: marker = str(row.get("marker") or "") type_code = str(row.get("type_code") or "") role_map = dict(FORM_ITEM_SEMANTIC_PROPERTIES.get(marker, {})) if marker == "34": name_path = str(row.get("name_path") or "") if name_path.endswith(".5"): role_map.pop(6, None) role_map[4] = ("Основные", "Вид") role_map[5] = ("Основные", "Имя") elif marker == "48": name_path = str(row.get("name_path") or "") if name_path.endswith(".6"): role_map.pop(7, None) role_map[5] = ("Основные", "Вид") role_map[6] = ("Основные", "Имя") elif marker in {"35", "37"} and str(row.get("name_path") or "").endswith(".7"): role_map = { (index + 1 if index >= 5 else index): value for index, value in role_map.items() } elif marker == "73": name_path = str(row.get("name_path") or "") if name_path.endswith(".5"): role_map.pop(6, None) role_map[5] = ("Основные", "Имя") groups: dict[str, list[dict[str, Any]]] = {} mapped: set[int] = set() for index, (group, name) in role_map.items(): mapped.add(index) value = parameter_value(parameters, index) if name == "Вид": value = item_type_name(marker, str(value) if value is not None else type_code) add_grouped_property(groups, group, semantic_property(name, value, index=index)) if marker == "31": add_grouped_property(groups, "Основные", semantic_property("Вид", "Кнопка командной панели", source="marker")) elif type_code and (6 if marker in {"35", "37"} and str(row.get("name_path") or "").endswith(".7") else 5) not in mapped: type_index = 6 if marker in {"35", "37"} and str(row.get("name_path") or "").endswith(".7") else 5 add_grouped_property(groups, "Основные", semantic_property("Вид", row.get("type_name") or item_type_name(marker, type_code), index=type_index)) mapped.add(type_index) return { "groups": groups, "unmapped_parameters": semantic_unmapped_parameters(parameters, mapped), "coverage": semantic_coverage(parameters, mapped), } def enrich_item_reference_semantics(records: list[dict[str, Any]]) -> None: by_name = {str(row.get("name") or ""): row for row in records if row.get("name")} by_id = {str(row.get("id") or ""): row for row in records if row.get("id") not in {None, ""}} for row in records: owner = str(row.get("name") or "") owner_path = str(row.get("path") or "") if not owner or not owner_path: continue descendants = [ candidate for candidate in records if str(candidate.get("path") or "").startswith(owner_path + ".") and candidate.get("name") != owner ] for suffix, prop_name in ( ("КонтекстноеМеню", "КонтекстноеМеню"), ("РасширеннаяПодсказка", "РасширеннаяПодсказка"), ("ExtendedTooltip", "РасширеннаяПодсказка"), ("КоманднаяПанель", "AutoCommandBar"), ): child = by_name.get(owner + suffix) if child is not None and child in descendants: add_derived_semantic_property(row, "Прочее", prop_name, child.get("name"), source="item_reference:name_path") for child in descendants: child_type = str(child.get("type_name") or "") if child_type in {"SearchStringAddition", "ViewStatusAddition", "SearchControlAddition"} and str(child.get("name") or "").startswith(owner): add_derived_semantic_property(row, "Прочее", child_type, child.get("name"), source="item_reference:child_type") user_settings_group_ids = [ *([str(row.pop("_user_settings_group_id"))] if row.get("_user_settings_group_id") not in {None, ""} else []), *[str(value) for value in row.pop("_user_settings_group_ids", [])], ] referenced_groups = [ by_id[value] for value in user_settings_group_ids if value in by_id and ( str(by_id[value].get("type_name") or "") in {"Группа", "Группа колонок", "Группа кнопок"} or "настрой" in str(by_id[value].get("name") or "").casefold() ) ] named_settings_groups = [ candidate for candidate in records if "настро" in str(candidate.get("name") or "").casefold() and "пользователь" in str(candidate.get("name") or "").casefold() and str(candidate.get("type_name") or "") == "Группа" ] user_settings_group = referenced_groups[-1] if referenced_groups else ( named_settings_groups[0] if str(row.get("marker") or "") == "55" and len(named_settings_groups) == 1 else None ) if user_settings_group is not None: add_derived_semantic_property( row, "Использование", "UserSettingsGroup", user_settings_group.get("name"), source="item_reference:id", ) addition_representations = { "SearchStringAddition": "SearchStringRepresentation", "ViewStatusAddition": "ViewStatusRepresentation", "SearchControlAddition": "SearchControl", } table_rows = [row for row in records if str(row.get("marker") or "") in {"55", "73"}] for row in records: representation = addition_representations.get(str(row.get("type_name") or "")) row_path = str(row.get("path") or "") if not representation or not row_path: continue owners = [ candidate for candidate in table_rows if str(candidate.get("path") or "") and row_path.startswith(str(candidate.get("path")) + ".") ] if not owners: continue owner = max(owners, key=lambda candidate: len(str(candidate.get("path") or "").split("."))) source = { "owner_element": owner.get("name"), "representation": representation, } row["addition_source"] = source add_derived_semantic_property(row, "Основные", "AdditionSource", source, source="item_hierarchy:table_addition") def enrich_input_field_specific_semantics(row: dict[str, Any], node: Any) -> None: marker = str(row.get("marker") or "") if marker not in {"35", "37"}: return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) legacy_offset = 1 if str(row.get("name_path") or "").endswith(".7") else 0 def actual_index(index: int) -> int: return index + legacy_offset if index >= 5 else index def field_scalar(index: int) -> str | None: return child_direct_scalar(node, actual_index(index)) title_location = title_location_presentation(field_scalar(7)) if title_location is not None: add_grouped_property(groups, "Основные", semantic_property("ПоложениеЗаголовка", title_location, index=actual_index(7), source="form_payload_input_field")) mark_semantic_parameter_mapped(semantic, actual_index(7)) visible = bool_presentation(field_scalar(43)) if visible is not None: add_grouped_property(groups, "Основные", semantic_property("Видимость", visible, index=actual_index(43), source="form_payload_input_field")) mark_semantic_parameter_mapped(semantic, actual_index(43)) enabled = bool_presentation(field_scalar(13)) if enabled is not None: add_grouped_property(groups, "Основные", semantic_property("Доступность", enabled, index=actual_index(13), source="form_payload_input_field")) mark_semantic_parameter_mapped(semantic, actual_index(13)) read_only = bool_presentation(field_scalar(14)) if read_only is not None: add_grouped_property(groups, "Основные", semantic_property("ТолькоПросмотр", read_only, index=actual_index(14), source="form_payload_input_field")) mark_semantic_parameter_mapped(semantic, actual_index(14)) skip_on_input = {"1": True, "2": False}.get(str(field_scalar(15) or "")) if skip_on_input is not None: add_grouped_property( groups, "Использование", semantic_property("ПропускатьПриВводе", skip_on_input, index=actual_index(15), source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, actual_index(15)) default_item = bool_presentation(field_scalar(16)) if default_item is not None: add_grouped_property( groups, "Основные", semantic_property( "АктивизироватьПоУмолчанию", default_item, index=actual_index(16), source="form_payload_input_field", ), ) mark_semantic_parameter_mapped(semantic, actual_index(16)) show_in_header = bool_presentation(field_scalar(20)) if show_in_header is not None: add_grouped_property( groups, "Расположение", semantic_property("ShowInHeader", show_in_header, index=actual_index(20), source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, actual_index(20)) show_in_footer = bool_presentation(field_scalar(21)) if show_in_footer is not None: add_grouped_property( groups, "Расположение", semantic_property("ShowInFooter", show_in_footer, index=actual_index(21), source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, actual_index(21)) fixing_in_table = "Left" if field_scalar(49) == "1" else None if fixing_in_table is not None: add_grouped_property( groups, "Расположение", semantic_property("FixingInTable", fixing_in_table, index=actual_index(49), source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, actual_index(49)) auto_cell_height = bool_presentation(field_scalar(28)) if auto_cell_height is True: add_grouped_property( groups, "Расположение", semantic_property("AutoCellHeight", True, index=actual_index(28), source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, actual_index(28)) cell_hyperlink = bool_presentation(field_scalar(22)) if cell_hyperlink is not None: add_grouped_property( groups, "Использование", semantic_property("CellHyperlink", cell_hyperlink, index=actual_index(22), source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, actual_index(22)) direct_properties = [ ("Расположение", "TitleHeight", field_scalar(8), actual_index(8)), ("Расположение", "HorizontalAlign", horizontal_align_presentation(field_scalar(23)), actual_index(23)), ("Использование", "РежимРедактирования", input_edit_mode_presentation(field_scalar(26)), actual_index(26)), ("Использование", "AutoEditMode", True if field_scalar(26) == "2" else None, actual_index(26)), ("Расположение", "GroupHorizontalAlign", horizontal_align_presentation(field_scalar(53)), actual_index(53)), ("Расположение", "GroupVerticalAlign", vertical_align_presentation(field_scalar(54)), actual_index(54)), ("Оформление", "ToolTipRepresentation", tooltip_representation_presentation(field_scalar(50)), actual_index(50)), ] for group, name, value, index in direct_properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_input_field")) mark_semantic_parameter_mapped(semantic, index) specific_index = actual_index(39) specific = children(node)[specific_index] if len(children(node)) > specific_index else None if specific is None: return choice_list = choice_list_presentation(children(specific)[1] if len(children(specific)) > 1 else None) if choice_list is not None: add_grouped_property( groups, "Использование", semantic_property("ChoiceList", choice_list, index=1, source="form_payload_field"), ) mark_semantic_parameter_mapped(semantic, 1) specific_marker = child_direct_scalar(specific, 0) if marker == "37" and str(row.get("type_code") or "") == "1" and specific_marker == "11": hyperlink = bool_presentation(child_direct_scalar(specific, 7)) if hyperlink is not None: add_grouped_property( groups, "Использование", semantic_property("Hiperlink", hyperlink, index=7, source="form_payload_label_field"), ) label_properties = [ ("Ширина", child_direct_scalar(specific, 1)), ("Высота", child_direct_scalar(specific, 2)), ("ВертикальноеПоложениеВГруппе", {"2": "Top"}.get(str(child_direct_scalar(specific, 3) or ""))), ("РастягиватьПоВертикали", bool_presentation(child_direct_scalar(specific, 4))), ("РастягиватьПоГоризонтали", bool_presentation(child_direct_scalar(specific, 17))), ] for name, value in label_properties: if value not in {None, "", "0"} or isinstance(value, bool): add_grouped_property( groups, "Расположение", semantic_property(name, value, index=39, source="form_payload_label_field"), ) mark_semantic_parameter_mapped(semantic, 39) text_color = style_value_presentation(child_at(specific, 8)) if text_color is not None: add_grouped_property( groups, "Оформление", semantic_property("TextColor", text_color, index=39, source="form_payload_label_field"), ) mark_semantic_parameter_mapped(semantic, 39) expected_specific_marker = "32" if marker == "35" else "36" if marker == "35" and specific_marker == "11": auto_max_width = bool_presentation(child_direct_scalar(specific, 15)) max_width = child_direct_scalar(specific, 16) if auto_max_width is not None: add_grouped_property( groups, "Расположение", semantic_property("АвтоМаксимальнаяШирина", auto_max_width, index=39, source="form_payload_label_field"), ) mark_semantic_parameter_mapped(semantic, 39) if max_width not in {None, ""}: add_grouped_property( groups, "Расположение", semantic_property("МаксимальнаяШирина", max_width, index=39, source="form_payload_label_field"), ) mark_semantic_parameter_mapped(semantic, 39) return if specific_marker != expected_specific_marker: if marker == "37" and str(row.get("type_code") or "") == "11" and specific_marker == "1": chart_height = child_direct_scalar(specific, 2) if chart_height not in {None, "", "0"}: add_grouped_property( groups, "Расположение", semantic_property("Высота", chart_height, index=39, source="form_payload_chart_field"), ) mark_semantic_parameter_mapped(semantic, 39) if marker == "37" and str(row.get("type_code") or "") == "14" and specific_marker == "3": graphical_properties = [ ("Расположение", "Ширина", child_direct_scalar(specific, 1)), ("Расположение", "Высота", child_direct_scalar(specific, 2)), ("Использование", "Edit", bool_presentation(child_direct_scalar(specific, 3))), ] for group, name, value in graphical_properties: if value not in {None, ""}: add_grouped_property( groups, group, semantic_property(name, value, index=39, source="form_payload_graphical_schema_field"), ) mark_semantic_parameter_mapped(semantic, 39) if marker == "37" and str(row.get("type_code") or "") == "4" and specific_marker == "10": picture_properties = [ ("Расположение", "Ширина", child_direct_scalar(specific, 1)), ("Расположение", "РастягиватьПоГоризонтали", bool_presentation(child_direct_scalar(specific, 2))), ("Использование", "РежимПеретаскиванияФайлов", {"1": "AsFile"}.get(str(child_direct_scalar(specific, 17) or ""))), ] for group, name, value in picture_properties: if value not in {None, ""}: add_grouped_property( groups, group, semantic_property(name, value, index=39, source="form_payload_picture_field"), ) mark_semantic_parameter_mapped(semantic, 39) return open_button = auto_bool_presentation(child_direct_scalar(specific, 15)) quick_choice = auto_bool_presentation(child_direct_scalar(specific, 23)) choose_type = bool_presentation(child_direct_scalar(specific, 32)) min_value = child_direct_scalar(child_at(specific, 16), 1) if child_direct_scalar(child_at(specific, 16), 0) == "N" else None max_value = child_direct_scalar(child_at(specific, 17), 1) if child_direct_scalar(child_at(specific, 17), 0) == "N" else None properties = [ ("Расположение", "Width", child_direct_scalar(specific, 2), 2), ("Расположение", "Height", child_direct_scalar(specific, 3), 3), ("Расположение", "HorizontalStretch", auto_bool_presentation(child_direct_scalar(specific, 4)), 4), ("Расположение", "VerticalStretch", auto_bool_presentation(child_direct_scalar(specific, 5)), 5), ("Расположение", "Wrap", bool_presentation(child_direct_scalar(specific, 6)), 6), ("Использование", "PasswordMode", {"1": True, "2": False}.get(str(child_direct_scalar(specific, 7) or "")), 7), ("Использование", "MultiLine", multiline_presentation(child_direct_scalar(specific, 8)), 8), ("Использование", "ChoiceListButton", bool_presentation(child_direct_scalar(specific, 11)), 11), ("Использование", "КнопкаВыпадающегоСписка", auto_bool_presentation(child_direct_scalar(specific, 47)), 47), ("Использование", "КнопкаВыбора", bool_presentation(child_direct_scalar(specific, 12)), 12), ("Использование", "SpinButton", auto_bool_presentation(child_direct_scalar(specific, 14)), 14), ("Использование", "OpenButton", open_button, 15), ("Использование", "КнопкаОчистки", auto_bool_presentation(child_direct_scalar(specific, 13)), 13), ("Использование", "CreateButton", bool_presentation(child_direct_scalar(specific, 45)), 45), ("Использование", "ListChoiceMode", bool_presentation(child_direct_scalar(specific, 19)), 19), ("Расположение", "ChoiceListHeight", child_direct_scalar(specific, 21), 21), ("Использование", "ChoiceFoldersAndItems", choice_folders_and_items_presentation(child_direct_scalar(specific, 24)), 24), ("Использование", "БыстрыйВыбор", quick_choice, 23), ("Использование", "AutoChoiceIncomplete", auto_bool_presentation(child_direct_scalar(specific, 28)), 28), ("Использование", "MarkRequiredComplete", auto_bool_presentation(child_direct_scalar(specific, 31)), 31), ("Использование", "AutoMarkIncomplete", auto_bool_presentation(child_direct_scalar(specific, 31)), 31), ("Использование", "ВыбиратьТип", choose_type, 32), ("Использование", "TypeDomainEnabled", choose_type, 32), ("Использование", "ExtendedEdit", bool_presentation(child_direct_scalar(specific, 52)), 52), ("Использование", "MinValue", min_value, 16), ("Использование", "MaxValue", max_value, 17), ("Использование", "РедактированиеТекста", bool_presentation(child_direct_scalar(specific, 41)), 41), ("Использование", "EditTextUpdate", edit_text_update_presentation(child_direct_scalar(specific, 43)), 43), ("Использование", "ChoiceButtonRepresentation", choice_button_representation_presentation(child_direct_scalar(specific, 46)), 46), ("Использование", "ChoiceHistoryOnInput", choice_history_on_input_presentation(child_direct_scalar(specific, 48)), 48), ("Использование", "ExtendedEditMultipleValues", bool_presentation(child_direct_scalar(specific, 65)), 65), ("Расположение", "AutoMaxHeight", bool_presentation(child_direct_scalar(specific, 52)), 52), ("Расположение", "MaxHeight", child_direct_scalar(specific, 53), 53), ("Расположение", "АвтоМаксимальнаяШирина", bool_presentation(child_direct_scalar(specific, 49)), 49), ("Расположение", "МаксимальнаяШирина", child_direct_scalar(specific, 50), 50), ("Расположение", "FooterHorizontalAlign", {"2": "Left"}.get(str(child_direct_scalar(specific, 45) or "")), 45), ] for group, name, value, index in properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_input_field")) mark_semantic_parameter_mapped(semantic, index) mask = child_direct_scalar(specific, 18) if mask: add_grouped_property( groups, "Использование", semantic_property("Mask", mask, index=18, source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, 18) for name, index in (("Format", 29), ("EditFormat", 30), ("InputHint", 44)): value_node = children(specific)[index] if len(children(specific)) > index else None localized = localized_text(value_node, "", max_depth=3) if value_node is not None else None value = str((localized or {}).get("value") or "") if value: group = "Форматирование" if name in {"Format", "EditFormat"} else "Оформление" add_grouped_property( groups, group, semantic_property(name, value, index=index, source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, index) for name, index in (("TextColor", 37), ("BackColor", 38), ("BorderColor", 39)): value = style_value_presentation(children(specific)[index] if len(children(specific)) > index else None) if value is not None: add_grouped_property( groups, "Оформление", semantic_property(name, value, index=index, source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, index) font = font_value_presentation(children(specific)[40] if len(children(specific)) > 40 else None) if font is not None: add_grouped_property( groups, "Оформление", semantic_property("Font", font, index=40, source="form_payload_input_field"), ) mark_semantic_parameter_mapped(semantic, 40) type_link = type_link_reference(children(specific)[42] if len(children(specific)) > 42 else None) if type_link is not None: row["_type_link_reference"] = type_link choice_parameter_links = choice_parameter_links_presentation( children(specific)[64] if len(children(specific)) > 64 else None ) if choice_parameter_links is not None: add_grouped_property( groups, "Использование", semantic_property( "ChoiceParameterLinks", choice_parameter_links, index=64, source="form_payload_input_field", ), ) mark_semantic_parameter_mapped(semantic, 64) def enrich_container_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "22": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) visible = bool_presentation(child_direct_scalar(node, 10)) visible_index = 10 if row.get("type_name") == "Группа": width = child_direct_scalar(node, 12) if width not in {None, "", "0"}: add_grouped_property( groups, "Расположение", semantic_property("Ширина", width, index=12, source="form_payload_group_layout"), ) mark_semantic_parameter_mapped(semantic, 12) for index in (26, 28): if child_direct_scalar(node, index) == "0": visible = False visible_index = index break if visible is not None: add_grouped_property(groups, "Основные", semantic_property("Видимость", visible, index=visible_index, source="form_payload_container")) mark_semantic_parameter_mapped(semantic, visible_index) if row.get("type_name") == "Командная панель": autofill_index = 28 if len(children(node)) == 29 else len(children(node)) - 1 autofill = bool_presentation(child_direct_scalar(node, autofill_index)) if autofill is not None: add_grouped_property( groups, "Использование", semantic_property("Автозаполнение", autofill, index=autofill_index, source="form_payload_command_bar"), ) mark_semantic_parameter_mapped(semantic, autofill_index) command_source = { "a9f3b1ac-f51b-431e-b102-55a69acdecad": "Form", }.get(str(child_direct_scalar(node, 22) or "").lower()) if command_source is not None: add_grouped_property( groups, "Использование", semantic_property("CommandSource", command_source, index=22, source="form_payload_command_bar"), ) mark_semantic_parameter_mapped(semantic, 22) if row.get("type_name") == "Группа кнопок": button_group_options = child_at(node, 20) command_source_guid = str(child_direct_scalar(child_at(button_group_options, 1), 1) or "").lower() command_source = { "02023637-7868-4a5f-8576-835a76e0c9ba": "Form", "2ef6d6fa-847a-485e-8684-d37a3ab5efb8": "FormCommandPanelGlobalCommands", }.get(command_source_guid) if child_direct_scalar(button_group_options, 0) == "2" and command_source is not None: add_grouped_property( groups, "Использование", semantic_property("CommandSource", command_source, index=20, source="form_payload_button_group"), ) mark_semantic_parameter_mapped(semantic, 20) container_representation = { "Группа кнопок": "Compact", "Подменю": "Picture", }.get(str(row.get("type_name") or "")) if container_representation: add_grouped_property( groups, "Оформление", semantic_property("Отображение", container_representation, source="form_payload_container_type"), ) if row.get("type_name") == "Страница": page_options = child_at(node, 20) page_grouping = { "1": "Horizontal", "2": "HorizontalIfPossible", }.get(str(child_direct_scalar(page_options, 16) or ""), "Vertical") add_grouped_property( groups, "Расположение", semantic_property("Группировка", page_grouping, index=20, source="form_payload_page_layout"), ) mark_semantic_parameter_mapped(semantic, 20) add_grouped_property(groups, "Использование", semantic_property("ScrollOnCompress", False, source="form_payload_container_type")) if child_direct_scalar(page_options, 6) == "0": add_grouped_property( groups, "Оформление", semantic_property("ПоказыватьЗаголовок", False, index=20, source="form_payload_page_layout"), ) for name, option_index in (("HorizontalSpacing", 10), ("VerticalSpacing", 11)): spacing = {"4": "OneAndHalf"}.get(str(child_direct_scalar(page_options, option_index) or "")) if spacing is not None: add_grouped_property( groups, "Расположение", semantic_property(name, spacing, index=20, source="form_payload_page_layout"), ) if row.get("type_name") == "Страницы": pages_options = children(node)[20] if len(children(node)) > 20 else None primary_representation = child_direct_scalar(pages_options, 1) repeated_representation = child_direct_scalar(pages_options, 5) compact_pages_layout = child_direct_scalar(pages_options, 0) == "3" and len(children(pages_options)) == 5 if primary_representation == repeated_representation or compact_pages_layout: pages_representation = { "0": "None", "1": "TabsOnTop", }.get(str(primary_representation or "")) if pages_representation is not None: add_grouped_property( groups, "Оформление", semantic_property( "PagesRepresentation", pages_representation, index=20, source="form_payload_pages_layout", ), ) mark_semantic_parameter_mapped(semantic, 20) if row.get("type_name") in {"Страница", "Страницы"}: horizontal_stretch = bool_presentation(child_direct_scalar(node, 14)) if horizontal_stretch is not None: add_grouped_property( groups, "Расположение", semantic_property("РастягиватьПоГоризонтали", horizontal_stretch, index=14, source="form_payload_container_layout"), ) mark_semantic_parameter_mapped(semantic, 14) if row.get("type_name") == "Группа колонок": column_options = child_at(node, 20) column_grouping = {"0": "Horizontal", "2": "InCell"}.get(str(child_direct_scalar(column_options, 1) or "")) if column_grouping is not None: add_grouped_property( groups, "Расположение", semantic_property("Группировка", column_grouping, index=20, source="form_payload_column_group"), ) mark_semantic_parameter_mapped(semantic, 20) if child_direct_scalar(node, 14) == "1": add_grouped_property( groups, "Расположение", semantic_property("РастягиватьПоГоризонтали", True, index=14, source="form_payload_column_group"), ) mark_semantic_parameter_mapped(semantic, 14) show_in_header = bool_presentation(child_direct_scalar(node, 19)) if show_in_header is not None: add_grouped_property( groups, "Расположение", semantic_property("ShowInHeader", show_in_header, index=19, source="form_payload_column_group"), ) mark_semantic_parameter_mapped(semantic, 19) if child_direct_scalar(column_options, 11) == "1": add_grouped_property( groups, "Расположение", semantic_property("FixingInTable", "Left", index=20, source="form_payload_column_group"), ) mark_semantic_parameter_mapped(semantic, 20) node_items = children(node) horizontal_align_index = len(node_items) - 3 if len(node_items) >= 29 else None vertical_align_index = len(node_items) - 2 if len(node_items) >= 29 else None horizontal_align = ( horizontal_align_presentation(child_direct_scalar(node, horizontal_align_index)) if horizontal_align_index is not None else None ) vertical_align = ( vertical_align_presentation(child_direct_scalar(node, vertical_align_index)) if vertical_align_index is not None else None ) if horizontal_align is not None: add_grouped_property( groups, "Расположение", semantic_property( "GroupHorizontalAlign", horizontal_align, index=horizontal_align_index, source="form_payload_container_layout_tail", ), ) mark_semantic_parameter_mapped(semantic, horizontal_align_index) if vertical_align is not None: add_grouped_property( groups, "Расположение", semantic_property( "GroupVerticalAlign", vertical_align, index=vertical_align_index, source="form_payload_container_layout_tail", ), ) mark_semantic_parameter_mapped(semantic, vertical_align_index) if row.get("type_name") == "Группа": horizontal_stretch = bool_presentation(child_direct_scalar(node, 14)) vertical_stretch = bool_presentation(child_direct_scalar(node, 15)) if horizontal_stretch is not None: add_grouped_property( groups, "Расположение", semantic_property("РастягиватьПоГоризонтали", horizontal_stretch, index=14, source="form_payload_group_layout"), ) mark_semantic_parameter_mapped(semantic, 14) if vertical_stretch is not None: add_grouped_property( groups, "Расположение", semantic_property("РастягиватьПоВертикали", vertical_stretch, index=15, source="form_payload_group_layout"), ) mark_semantic_parameter_mapped(semantic, 15) managed_group_options = child_at(node, 20) managed_group_layout = child_direct_scalar(managed_group_options, 0) == "29" legacy_group_mode = child_direct_scalar(node, 22) compact_vertical_weak = len(node_items) == 30 and child_direct_scalar(node, 21) == "0" and child_direct_scalar(node, 24) == "1" if managed_group_layout: primary_group = child_direct_scalar(managed_group_options, 1) grouping_variant = child_direct_scalar(managed_group_options, 4) extended_grouping_variant = child_direct_scalar(managed_group_options, 22) representation_group = child_direct_scalar(managed_group_options, 3) layout_group = None grouping_index = 20 representation_index = 20 grouping = ( "Vertical" if primary_group == "0" else ("HorizontalIfPossible" if grouping_variant == "1" or extended_grouping_variant == "2" else "AlwaysHorizontal") ) if child_direct_scalar(managed_group_options, 27) == "1": grouping = "Horizontal" representation = {"0": "None", "2": "WeakSeparation", "3": "NormalSeparation"}.get(str(representation_group or ""), "None") group_mode = grouping_variant united = bool_presentation(child_direct_scalar(managed_group_options, 21)) if united is not None: add_grouped_property( groups, "Прочее", semantic_property("United", united, index=20, source="form_payload_managed_group_layout"), ) back_color = style_value_presentation(children(managed_group_options)[9] if len(children(managed_group_options)) > 9 else None) if back_color is not None: add_grouped_property( groups, "Оформление", semantic_property("ЦветФона", back_color, index=20, source="form_payload_managed_group_layout"), ) child_items_width = {"1": "Equal", "5": "LeftNarrowest"}.get(str(child_direct_scalar(managed_group_options, 2) or "")) if child_items_width is not None: add_grouped_property( groups, "Расположение", semantic_property("ChildItemsWidth", child_items_width, index=20, source="form_payload_managed_group_layout"), ) vertical_spacing = {"2": "Half"}.get(str(child_direct_scalar(managed_group_options, 16) or "")) if vertical_spacing is not None: add_grouped_property( groups, "Расположение", semantic_property("VerticalSpacing", vertical_spacing, index=20, source="form_payload_managed_group_layout"), ) horizontal_align = {"3": "Center"}.get(str(child_direct_scalar(managed_group_options, 27) or "")) if horizontal_align is not None: add_grouped_property( groups, "Расположение", semantic_property("ГоризонтальноеПоложениеВГруппе", horizontal_align, index=20, source="form_payload_managed_group_layout"), ) managed_horizontal_align = horizontal_align_presentation(child_direct_scalar(managed_group_options, 17)) if managed_horizontal_align not in {None, "Auto"}: groups["Расположение"] = [ prop for prop in groups.get("Расположение", []) if prop.get("name") != "GroupHorizontalAlign" ] add_grouped_property( groups, "Расположение", semantic_property( "ГоризонтальноеПоложениеВГруппе", managed_horizontal_align, index=20, source="form_payload_managed_group_layout", ), ) tooltip_representation = {"1": "Button", "2": "None"}.get(str(child_direct_scalar(managed_group_options, 22) or "")) if tooltip_representation is not None: add_grouped_property( groups, "Оформление", semantic_property("ОтображениеПодсказки", tooltip_representation, index=20, source="form_payload_managed_group_layout"), ) elif compact_vertical_weak: group_mode = "0" primary_group = None representation_group = None layout_group = child_direct_scalar(node, 26) grouping_index = 21 representation_index = 24 grouping = "Vertical" representation = "WeakSeparation" elif legacy_group_mode in {"2", "3"}: group_mode = legacy_group_mode primary_group = child_direct_scalar(node, 23) representation_group = child_direct_scalar(node, 25) layout_group = child_direct_scalar(node, 27) grouping_index = 22 representation_index = 25 else: group_mode = child_direct_scalar(node, 21) primary_group = child_direct_scalar(node, 22) representation_group = child_direct_scalar(node, 24) layout_group = child_direct_scalar(node, 26) grouping_index = 21 representation_index = 24 if not managed_group_layout and not compact_vertical_weak: weak_representation = bool(primary_group and representation_group and primary_group != representation_group) typed_primary_group = child_scalar(node, 22) typed_representation_group = child_scalar(node, 24) compact_horizontal_if_possible = ( group_mode == "2" and child_direct_scalar(node, 12) == "0" and typed_primary_group == "3d3cb80c-508b-41fa-8a18-680cdf5f1712" and typed_representation_group == "77ffcc29-7f2d-4223-b22f-19666e7250ba" ) grouping = ( "HorizontalIfPossible" if compact_horizontal_if_possible else ("Vertical" if weak_representation else ("HorizontalIfPossible" if group_mode == "3" else "AlwaysHorizontal")) ) representation = "WeakSeparation" if weak_representation or compact_horizontal_if_possible else "None" else: weak_representation = True add_grouped_property(groups, "Расположение", semantic_property("Группировка", grouping, index=grouping_index, source="form_payload_group_layout")) add_grouped_property(groups, "Поведение", semantic_property("Поведение", "Usual", source="form_payload_group_layout")) add_grouped_property(groups, "Оформление", semantic_property("Отображение", representation, index=representation_index, source="form_payload_group_layout")) if managed_group_layout: add_grouped_property(groups, "Оформление", semantic_property("ПоказыватьЗаголовок", False, index=20, source="form_payload_managed_group_layout")) mark_semantic_parameter_mapped(semantic, 20) elif compact_vertical_weak: add_grouped_property(groups, "Оформление", semantic_property("ПоказыватьЗаголовок", False, source="live_sql_compact_group_layout")) elif not weak_representation and layout_group: add_grouped_property(groups, "Оформление", semantic_property("ПоказыватьЗаголовок", False, source="form_payload_group_layout")) def enrich_table_addition_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "6": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) properties = [ ("Основные", "Видимость", bool_presentation(child_direct_scalar(node, 9)), 9), ("Основные", "Доступность", bool_presentation(child_direct_scalar(node, 10)), 10), ] for group, name, value, index in properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_table_addition")) mark_semantic_parameter_mapped(semantic, index) def enrich_dynamic_list_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "55": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) add_grouped_property(groups, "Основные", semantic_property("Вид", "Динамический список", source="form_payload_dynamic_list")) visible = bool_presentation(child_direct_scalar(node, 13)) if visible is not None: add_grouped_property(groups, "Основные", semantic_property("Видимость", visible, index=13, source="form_payload_dynamic_list")) mark_semantic_parameter_mapped(semantic, 13) properties = [ ("Основные", "ПоложениеКоманднойПанели", {"0": "None", "1": "Top", "2": "Bottom"}.get(str(child_direct_scalar(node, 6) or "")), 6), ("Основные", "АктивизироватьПоУмолчанию", bool_presentation(child_direct_scalar(node, 16)), 16), ("Основные", "ТолькоПросмотр", bool_presentation(child_direct_scalar(node, 14)), 14), ("Использование", "ПропускатьПриВводе", bool_presentation(child_direct_scalar(node, 15)), 15), ("Использование", "ChangeRowSet", bool_presentation(child_direct_scalar(node, 17)), 17), ("Использование", "ChangeRowOrder", bool_presentation(child_direct_scalar(node, 18)), 18), ("Оформление", "Отображение", "List", None), ("Оформление", "ЦветРамки", "style:BorderColor", None), ("Расположение", "HeightInTableRows", child_direct_scalar(node, 21), 21), ("Оформление", "Footer", bool_presentation(child_direct_scalar(node, 28)), 28), ("Использование", "RowSelectionMode", row_selection_mode_presentation(child_direct_scalar(node, 31)), 31), ("Оформление", "HorizontalLinesBWA", bool_presentation(child_direct_scalar(node, 33)), 33), ("Оформление", "VerticalLinesBWA", bool_presentation(child_direct_scalar(node, 34)), 34), ("Оформление", "UseAlternationRowColorBWA", bool_presentation(child_direct_scalar(node, 35)), 35), ("Использование", "AutoInsertNewRow", bool_presentation(child_direct_scalar(node, 42)), 42), ("Использование", "EnableStartDrag", bool_presentation(child_direct_scalar(node, 41)), 41), ("Использование", "EnableDrag", bool_presentation(child_direct_scalar(node, 53)), 53), ] for group, name, value, index in properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_dynamic_list")) if index is not None: mark_semantic_parameter_mapped(semantic, index) property_count = child_direct_scalar(node, 54) if property_count and property_count.isdigit(): pair_count = int(property_count) property_values: dict[str, tuple[Any, int]] = {} property_keys: set[str] = set() pair_index = 55 for _ in range(pair_count): key = child_direct_scalar(node, pair_index) if key: property_keys.add(key) value_node = child_at(node, pair_index + 1) value_kind = child_direct_scalar(value_node, 0) if key and value_kind == "B": property_values[key] = (bool_presentation(child_direct_scalar(value_node, 1)), pair_index + 1) elif key and value_kind == "N": property_values[key] = (child_direct_scalar(value_node, 1), pair_index + 1) elif key and value_kind == "#": encoded_value = child_direct_scalar(value_node, 2) if encoded_value is None and key in {"14", "16"}: encoded_value = next((value for value in reversed(atoms(value_node, limit=20)) if value in {"0", "1", "2"}), None) property_values[key] = (encoded_value, pair_index + 1) elif key == "16": value_atoms = atoms(value_node, limit=20) encoded_value = next((value for value in reversed(value_atoms) if value in {"0", "1"}), None) if encoded_value is None and "U" in value_atoms: encoded_value = "1" if encoded_value is None: encoded_value = "1" property_values[key] = (encoded_value, pair_index + 1) pair_index += 2 dynamic_properties = [ ("Использование", "AutoRefresh", property_values.get("5")), ("Использование", "AutoRefreshPeriod", property_values.get("6")), ("Использование", "ChoiceFoldersAndItems", property_values.get("8")), ("Использование", "RestoreCurrentRow", property_values.get("9")), ("Использование", "ShowRoot", property_values.get("11")), ("Использование", "AllowRootChoice", property_values.get("12")), ("Использование", "UpdateOnDataChange", property_values.get("14")), ("Использование", "AllowGettingCurrentRowURL", property_values.get("16")), ] for group, name, value_and_index in dynamic_properties: if value_and_index is None: continue value, index = value_and_index if name == "ChoiceFoldersAndItems": value = choice_folders_and_items_presentation(value) elif name == "UpdateOnDataChange": value = {"0": "Auto", "1": "Always", "2": "Never"}.get(str(value or "")) elif name == "AllowGettingCurrentRowURL": value = bool_presentation(value) if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_dynamic_list_property_bag")) mark_semantic_parameter_mapped(semantic, index) if "16" in property_keys and "AllowGettingCurrentRowURL" not in { str(prop.get("name") or "") for prop in groups.get("Использование", []) }: add_grouped_property( groups, "Использование", semantic_property( "AllowGettingCurrentRowURL", True, source="form_payload_dynamic_list_property_bag_default", ), ) user_settings_group = child_at(node, pair_index) user_settings_group_ids = [value for value in atoms(user_settings_group, limit=20) if value not in {"", "0"} and value.isdigit()] if user_settings_group_ids: row["_user_settings_group_ids"] = user_settings_group_ids mark_semantic_parameter_mapped(semantic, pair_index) initial_tree_view_node = child_at(node, pair_index + 1) initial_tree_view = {"0": "ExpandTopLevel", "1": "DoNotExpand", "2": "ExpandAll"}.get( str(child_direct_scalar(initial_tree_view_node, 0) or "") ) if initial_tree_view is not None: add_grouped_property( groups, "Использование", semantic_property("InitialTreeView", initial_tree_view, index=pair_index + 1, source="form_payload_dynamic_list_tail"), ) mark_semantic_parameter_mapped(semantic, pair_index + 1) row_picture_field = child_direct_scalar(node, pair_index + 2) if row_picture_field not in {None, "", "0"} and row.get("name"): add_grouped_property( groups, "Использование", semantic_property( "RowPictureDataPath", f"{row.get('name')}.DefaultPicture", index=pair_index + 2, source="form_payload_dynamic_list_standard_field_reference", ), ) mark_semantic_parameter_mapped(semantic, pair_index + 2) items = children(node) has_location_tail = ( len(items) >= 30 and child_direct_scalar(child_at(node, len(items) - 26), 0) == "12" and child_direct_scalar(child_at(node, len(items) - 21), 0) == "5" and child_direct_scalar(child_at(node, len(items) - 19), 0) == "5" and child_direct_scalar(child_at(node, len(items) - 17), 0) == "5" ) if has_location_tail: relative_properties = [ ("Расположение", "SearchStringLocation", search_string_location_presentation, -25), ("Расположение", "ViewStatusLocation", view_status_location_presentation, -24), ("Расположение", "SearchControlLocation", search_control_location_presentation, -23), ("Использование", "FileDragMode", file_drag_mode_presentation, -2), ] for group, name, presenter, offset in relative_properties: raw_value, index = child_direct_scalar_from_end(node, offset) value = presenter(raw_value) if value is not None and index is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_dynamic_list_tail")) mark_semantic_parameter_mapped(semantic, index) def row_selection_mode_presentation(value: str | None) -> str | None: return {"2": "Cell"}.get(str(value) if value is not None else "") def search_string_location_presentation(value: str | None) -> str | None: return { "0": "Default", "1": "None", "2": "CommandBar", "3": "Top", "5": "FormCaption", "6": "PullFromTop", }.get(str(value) if value is not None else "") def view_status_location_presentation(value: str | None) -> str | None: return {"0": "Default", "1": "None", "2": "Top"}.get(str(value) if value is not None else "") def search_control_location_presentation(value: str | None) -> str | None: return {"0": "Default", "1": "None", "2": "CommandBar"}.get(str(value) if value is not None else "") def file_drag_mode_presentation(value: str | None) -> str | None: return {"0": "AsFile", "1": "Default"}.get(str(value) if value is not None else "") def table_height_in_rows_parameter(node: Any) -> tuple[str | None, int]: live_value = child_direct_scalar(node, 38) legacy_value = child_direct_scalar(node, 39) if live_value not in {None, "", "0"} and legacy_value in {None, "", "0"}: return live_value, 38 return legacy_value, 39 def enrich_table_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "73": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) height_value, height_index = table_height_in_rows_parameter(node) enable_start_drag_value = bool_presentation(child_direct_scalar(node, 43)) enable_start_drag_index = 43 if enable_start_drag_value is None: enable_start_drag_value = bool_presentation(child_direct_scalar(node, 41)) enable_start_drag_index = 41 properties = [ ("Основные", "Видимость", False if child_direct_scalar(node, 72) == "0" else None, 72), ("Основные", "Доступность", False if child_direct_scalar(node, 13) == "0" else None, 13), ("Оформление", "Отображение", "List", None), ("Расположение", "HeightInTableRows", height_value, height_index), ("Использование", "RowSelectionMode", row_selection_mode_presentation(child_direct_scalar(node, 31)), 31), ("Оформление", "HorizontalLinesBWA", bool_presentation(child_direct_scalar(node, 33)), 33), ("Оформление", "VerticalLinesBWA", bool_presentation(child_direct_scalar(node, 34)), 34), ("Оформление", "UseAlternationRowColorBWA", bool_presentation(child_direct_scalar(node, 35)), 35), ("Использование", "AutoInsertNewRow", bool_presentation(child_direct_scalar(node, 42)), 42), ("Использование", "EnableStartDrag", enable_start_drag_value, enable_start_drag_index), ("Использование", "EnableDrag", bool_presentation(child_direct_scalar(node, 53)), 53), ] for group, name, value, index in properties: if value not in {None, ""}: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_table")) if index is not None: mark_semantic_parameter_mapped(semantic, index) def enrich_command_button_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "31": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) command_bar_location_raw = child_direct_scalar(node, 14) command_bar_location = command_bar_location_presentation(command_bar_location_raw) if command_bar_location_raw == "2": command_bar_location = "InCommandBar" if child_direct_scalar(node, 15) == "1" else "В дополнительном подменю" properties = [ ("Основные", "Видимость", {"0": False, "1": True, "2": True}.get(str(child_direct_scalar(node, 49) or "")), 49), ("Основные", "Отображение", button_representation_presentation(child_direct_scalar(node, 10)), 10), ("Основные", "Доступность", bool_presentation(child_direct_scalar(node, 31)), 31), ("Основные", "ButtonImportance", button_importance_presentation(child_direct_scalar(node, 11)), 11), ("Основные", "КнопкаПоУмолчанию", bool_presentation(child_direct_scalar(node, 11)), 11), ("Использование", "ПропускатьПриВводе", {"0": False, "1": True, "2": False}.get(str(child_direct_scalar(node, 29) or "")), 29), ("Расположение", "ПоложениеВКоманднойПанели", command_bar_location, 14), ("Расположение", "УникальностьКоманды", bool_presentation(child_direct_scalar(node, 26)), 26), ("Расположение", "GroupHorizontalAlign", horizontal_align_presentation(child_direct_scalar(node, 41)), 41), ("Расположение", "GroupVerticalAlign", vertical_align_presentation(child_direct_scalar(node, 42)), 42), ("Оформление", "ЦветРамки", auto_enum_presentation(child_direct_scalar(node, 43)), 43), ("Оформление", "ShapeRepresentation", "None" if child_direct_scalar(node, 45) == "3" else None, 45), ("Использование", "ToolTipRepresentation", {"1": "Button", "2": "Balloon"}.get(str(child_direct_scalar(node, 50) or "")), 50), ] for group, name, value, index in properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_command_button")) mark_semantic_parameter_mapped(semantic, index) def enrich_decoration_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "12": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) width = child_direct_scalar(node, 10) specific = child_at(node, 18) properties = [ ("Ширина", width if width not in {None, "", "0"} else None, 10, "Расположение"), ("Высота", child_direct_scalar(node, 11) if child_direct_scalar(node, 11) not in {None, "", "0"} else None, 11, "Расположение"), ("РастягиватьПоГоризонтали", auto_bool_presentation(child_direct_scalar(node, 12)), 12, "Расположение"), ("РастягиватьПоВертикали", auto_bool_presentation(child_direct_scalar(node, 13)), 13, "Расположение"), ("ЦветТекста", style_value_presentation(child_at(node, 14)), 14, "Оформление"), ("ЦветФона", style_value_presentation(child_at(specific, 6)), 18, "Оформление"), ("Видимость", bool_presentation(child_direct_scalar(node, 21)), 21, "Основные"), ("АвтоМаксимальнаяШирина", bool_presentation(child_direct_scalar(node, 27)), 27, "Расположение"), ("МаксимальнаяШирина", child_direct_scalar(node, 28) if child_direct_scalar(node, 28) not in {None, "", "0"} else None, 28, "Расположение"), ("АвтоМаксимальнаяВысота", bool_presentation(child_direct_scalar(node, 29)), 29, "Расположение"), ("МаксимальнаяВысота", child_direct_scalar(node, 31) if child_direct_scalar(node, 31) not in {None, "", "0"} else None, 31, "Расположение"), ( "ГоризонтальноеПоложениеВГруппе", horizontal_align_presentation(child_direct_scalar(specific, 2)) if child_direct_scalar(specific, 2) not in {None, "", "0"} else None, 18, "Расположение", ), ("ОтображениеПодсказки", {"2": "Balloon"}.get(str(child_direct_scalar(node, 22) or "")), 22, "Оформление"), ("Гиперссылка", bool_presentation(child_direct_scalar(specific, 1)), 18, "Использование"), ("GroupHorizontalAlign", horizontal_align_presentation(child_direct_scalar(node, 32)), 32), ("GroupVerticalAlign", vertical_align_presentation(child_direct_scalar(node, 33)), 33), ] for item in properties: name, value, index = item[:3] group = item[3] if len(item) > 3 else "Расположение" if value is not None: add_grouped_property( groups, group, semantic_property(name, value, index=index, source="form_payload_decoration"), ) mark_semantic_parameter_mapped(semantic, index) def enrich_radio_button_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "37" or str(row.get("type_code") or "") != "5": return specific = child_at(node, 39) if child_direct_scalar(specific, 0) != "8": return radio_button_type = {"0": "Auto", "2": "Tumbler"}.get(str(child_direct_scalar(specific, 7) or "")) if radio_button_type is None: return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) add_grouped_property( groups, "Прочее", semantic_property("RadioButtonType", radio_button_type, index=39, source="form_payload_radio_button"), ) columns_count = child_direct_scalar(specific, 2) if columns_count not in {None, "", "0"}: add_grouped_property( groups, "Расположение", semantic_property("ColumnsCount", columns_count, index=39, source="form_payload_radio_button"), ) mark_semantic_parameter_mapped(semantic, 39) def enrich_form_button_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "34": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) properties = [ ("Основные", "Видимость", False if child_direct_scalar(node, 26) == "0" else None, 26), ("Основные", "Доступность", False if child_direct_scalar(node, 7) == "0" else None, 7), ] for group, name, value, index in properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_button")) mark_semantic_parameter_mapped(semantic, index) def form_button_command_binding(node: Any, base_path: str) -> dict[str, Any] | None: if child_direct_scalar(node, 0) != "34": return None binding = children(node)[8] if len(children(node)) > 8 else None binding_items = children(binding) if len(binding_items) < 2: return None command_id = direct_scalar(binding_items[0]) group_guid = direct_scalar(binding_items[1]) if command_id in {None, ""} or group_guid in {None, ""}: return None group_guid = str(group_guid).lower() result: dict[str, Any] = { "command_id": str(command_id), "group_guid": group_guid, "path": path_join(base_path, 8), "command_id_path": path_join(path_join(base_path, 8), 0), "group_guid_path": path_join(path_join(base_path, 8), 1), } standard_name = FORM_STANDARD_COMMANDS.get((group_guid, str(command_id))) if standard_name: result.update({"command_name": standard_name, "scope": "standard", "match_by": "standard_command_guid"}) elif group_guid == FORM_LOCAL_COMMAND_GROUP_GUID: result.update({"scope": "form", "match_by": "form_command_id"}) else: result.update({"scope": "unknown", "match_by": "command_binding"}) return result def form_command_reference(node: Any, base_path: str) -> dict[str, Any] | None: """Expose metadata-command references used by command-bar buttons.""" if child_direct_scalar(node, 0) != "31": return None reference = children(node)[8] if len(children(node)) > 8 else None values = atoms(reference, limit=10) command_guid = next((value.lower() for value in values if GUID_RE.fullmatch(value or "")), None) if not command_guid: return None command_code = next((value for value in values if re.fullmatch(r"-?\d+", value or "")), None) result = { "guid": command_guid, **({"code": command_code} if command_code is not None else {}), "path": path_join(base_path, 8), "scope": "metadata", "status": "identity_pending", } standard_name = FORM_STANDARD_COMMANDS.get((command_guid, str(command_code or ""))) if standard_name: result.update({"command_name": standard_name, "scope": "standard", "status": "ok", "match_by": "standard_command_guid"}) elif command_guid == FORM_LOCAL_COMMAND_GROUP_GUID and command_code is not None: result.update({"command_id": command_code, "scope": "form", "status": "ok", "match_by": "form_command_id"}) return result def edit_mode_presentation(value: str | None) -> str | None: return { "3": "EnterOnInput", }.get(str(value) if value is not None else "") def multiline_presentation(value: str | None) -> bool | None: if value == "1": return True if value == "2": return False return None def form_field_details_node(node: Any) -> tuple[Any | None, int | None]: for index in (39, 40): details = children(node)[index] if len(children(node)) > index else None if details is not None and child_direct_scalar(details, 0) == "38": return details, index return None, None def enrich_form_field_specific_semantics(row: dict[str, Any], node: Any) -> None: if str(row.get("marker") or "") != "48": return semantic = row.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) field_properties = [ ("Основные", "ПоложениеЗаголовка", "None" if child_direct_scalar(node, 7) == "0" else None, 7), ("Основные", "Видимость", False if child_direct_scalar(node, 43) == "0" else None, 43), ("Основные", "Доступность", False if child_direct_scalar(node, 13) == "0" else None, 13), ("Основные", "ТолькоПросмотр", True if child_direct_scalar(node, 14) == "1" else None, 14), ("Основные", "ПропускатьПриВводе", True if child_direct_scalar(node, 15) == "1" else None, 15), ] for group, name, value, index in field_properties: if value is not None: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_field")) mark_semantic_parameter_mapped(semantic, index) details, details_index = form_field_details_node(node) if details is not None and details_index is not None: auto_max_width = bool_presentation(child_direct_scalar(details, 49)) multiline = multiline_presentation(child_direct_scalar(details, 8)) properties = [ ("Расположение", "Ширина", child_direct_scalar(details, 2), details_index), ("Расположение", "Высота", child_direct_scalar(details, 3), details_index), ("Расположение", "АвтоМаксимальнаяШирина", False if auto_max_width is False else None, details_index), ("Использование", "MultiLine", True if multiline is True else None, details_index), ] for group, name, value, index in properties: if value not in {None, "", "0"}: add_grouped_property(groups, group, semantic_property(name, value, index=index, source="form_payload_field_details")) edit_mode_index = 62 edit_mode = edit_mode_presentation(child_direct_scalar(node, edit_mode_index)) if edit_mode is None: edit_mode_index = 61 edit_mode = edit_mode_presentation(child_direct_scalar(node, edit_mode_index)) if edit_mode is not None: add_grouped_property(groups, "Использование", semantic_property("РежимРедактирования", edit_mode, index=edit_mode_index, source="form_payload_field")) mark_semantic_parameter_mapped(semantic, edit_mode_index) auto_edit_index = edit_mode_index + 1 auto_edit = bool_presentation(child_direct_scalar(node, auto_edit_index)) if edit_mode is not None and auto_edit is not None: add_grouped_property(groups, "Использование", semantic_property("AutoEditMode", auto_edit, index=auto_edit_index, source="form_payload_field")) mark_semantic_parameter_mapped(semantic, auto_edit_index) def section_record_semantic_properties(row: dict[str, Any], parameters: list[dict[str, Any]], node: Any = None) -> dict[str, Any]: groups: dict[str, list[dict[str, Any]]] = {} mapped: set[int] = set() for index, (group, name) in SECTION_RECORD_SEMANTIC_PROPERTIES.items(): mapped.add(index) add_grouped_property(groups, group, semantic_property(name, parameter_value(parameters, index), index=index)) if row.get("category"): add_grouped_property(groups, "Основные", semantic_property("Категория", row.get("category"), source="decoder")) add_grouped_property(groups, "Основные", semantic_property("Вид", row.get("category"), source="decoder")) if row.get("category") == "Command" and row.get("name"): raw_action = parameter_value(parameters, 8) explicit_action = str(raw_action or "").strip() action = explicit_action if BSL_IDENTIFIER_RE.fullmatch(explicit_action) else str(row.get("name") or "") row["action"] = action row["action_source"] = "form_payload_parameter_8" if explicit_action == action else "command_name_fallback" if explicit_action == action: mapped.add(8) add_grouped_property( groups, "Основные", semantic_property("Action", action, index=8 if explicit_action == action else None, source=row["action_source"]), ) current_row_code = str(parameter_value(parameters, 9) or "") if current_row_code: current_row_use = {"1": "DontUse", "2": "DontUse", "3": "DontUse"}.get(current_row_code, current_row_code) add_grouped_property(groups, "Использование", semantic_property("CurrentRowUse", current_row_use, index=9, source="form_payload_command")) mapped.add(9) modifies_saved_data = bool_presentation(str(parameter_value(parameters, 10) or "")) if modifies_saved_data is not None: add_grouped_property(groups, "Использование", semantic_property("ModifiesSavedData", modifies_saved_data, index=10, source="form_payload_command")) mapped.add(10) representation = {"1": "Picture", "2": "TextPicture"}.get(str(child_direct_scalar(node, 9) or "")) if representation is not None: add_grouped_property(groups, "Основные", semantic_property("Отображение", representation, index=9, source="form_payload_command")) shortcut = shortcut_presentation(child_at(node, 6)) if shortcut is not None: add_grouped_property(groups, "Использование", semantic_property("СочетаниеКлавиш", shortcut, index=6, source="form_payload_command")) mapped.add(6) if row.get("category") == "Attribute": main_attribute = bool_presentation(str(parameter_value(parameters, 10) or "")) if main_attribute is None and row.get("index") == 0 and str(row.get("name") or "") == "Объект": main_attribute = True if main_attribute is not None: add_grouped_property(groups, "Основные", semantic_property("MainAttribute", main_attribute, index=10 if parameter_value(parameters, 10) is not None else None, source="form_payload_attribute")) if parameter_value(parameters, 10) is not None: mapped.add(10) saved_data = bool_presentation(str(parameter_value(parameters, 11) or "")) if saved_data is not None: add_grouped_property(groups, "Использование", semantic_property("SavedData", saved_data, index=11, source="form_payload_attribute")) mapped.add(11) fill_check = {"1": "ShowError"}.get(str(parameter_value(parameters, 12) or "")) if fill_check is not None: add_grouped_property(groups, "Использование", semantic_property("FillCheck", fill_check, index=12, source="form_payload_attribute")) mapped.add(12) return { "groups": groups, "unmapped_parameters": semantic_unmapped_parameters(parameters, mapped), "coverage": semantic_coverage(parameters, mapped), } def first_scalar_with_path(node: Any, base_path: str, *, skip_guids: bool = False) -> dict[str, str] | None: direct = scalar(node) if direct not in {None, ""} and (not skip_guids or not GUID_RE.fullmatch(direct)): return {"value": direct, "path": base_path} for index, child in enumerate(children(node)): found = first_scalar_with_path(child, path_join(base_path, index), skip_guids=skip_guids) if found: return found return None def localized_text(node: Any, base_path: str, *, max_depth: int = 2) -> dict[str, str] | None: def walk(value: Any, path: str, depth: int) -> dict[str, str] | None: if depth > max_depth: return None items = children(value) for index in range(len(items) - 1): lang = scalar(items[index]) text = scalar(items[index + 1]) if lang and text and re.fullmatch(r"[a-z]{2}(?:[-_][A-Z]{2})?", lang): return {"lang": lang, "value": text, "path": path_join(path, index + 1)} for index, child in enumerate(items): found = walk(child, path_join(path, index), depth + 1) if found: return found return None return walk(node, base_path, 0) def record_id(node: Any, base_path: str) -> dict[str, str] | None: items = children(node) if len(items) > 1: direct = first_scalar_with_path(items[1], path_join(base_path, 1)) if direct: return direct return None def reference_id_from_node(node: Any) -> str | None: values = atoms(node, limit=20) numbers = [value for value in values if re.fullmatch(r"-?\d+", value or "")] return numbers[-1] if numbers else None def data_path_reference_from_node(node: Any) -> dict[str, str]: items = children(node) if len(items) >= 3 and scalar(items[0]) == "2": owner_id = reference_id_from_node(items[1]) field_id = reference_id_from_node(items[2]) if owner_id and field_id: return {"attribute_id": owner_id, "field_id": field_id} reference = reference_id_from_node(node) return {"attribute_id": reference} if reference else {} def footer_data_path_reference(primary_node: Any, footer_node: Any) -> dict[str, Any] | None: primary_items = children(primary_node) primary_candidate = primary_items[3] if len(primary_items) > 3 else None primary_aggregate = ( child_direct_scalar(primary_node, 0) == "3" and str(child_direct_scalar(primary_candidate, 0) or "").startswith("101") ) aggregate_owner = primary_node if primary_aggregate else footer_node if child_direct_scalar(aggregate_owner, 0) != "3": return None aggregate_items = children(aggregate_owner) aggregate_node = aggregate_items[3] if len(aggregate_items) > 3 else None aggregate_code = child_direct_scalar(aggregate_node, 0) field_guid = child_direct_scalar(aggregate_node, 1) if not aggregate_code or not aggregate_code.startswith("101") or not field_guid or not GUID_RE.fullmatch(field_guid): return None if not primary_aggregate: primary_guids = guids(primary_node, limit=8) if not primary_guids or primary_guids[-1] != field_guid.lower(): return None result: dict[str, Any] = {"aggregate": "Total", "field_guid": field_guid.lower()} if primary_aggregate: result["primary_aggregate"] = True return result def record_name(node: Any, *, category: str | None = None) -> str | None: items = children(node) marker = child_scalar(node, 0) name_indexes = [3, 6] if category == "Attribute" and marker == "9" else [2, 6, 3] for index in name_indexes: if index >= len(items): continue name = scalar(items[index]) if name and not GUID_RE.fullmatch(name) and not re.fullmatch(r"-?\d+(?:\.\d+)?", name): return name strings = collect_strings(node, limit=20) return next((item for item in strings if item and item not in {"#", "Pattern", "B", "U", "S", "N", "D", "ru"}), None) def form_parameter_rows(tree: Any, *, include_parameters: bool = True, max_parameters: int = 80) -> list[dict[str, Any]]: section = child_at(tree, 4) if child_direct_scalar(section, 0) != "0": return [] result: list[dict[str, Any]] = [] for index, node in enumerate(children(section)[2:]): if child_direct_scalar(node, 0) != "0": continue name = str(child_direct_scalar(node, 1) or "") if not name or not BSL_IDENTIFIER_RE.fullmatch(name): continue key_parameter = bool_presentation(child_direct_scalar(node, 3)) groups: dict[str, list[dict[str, Any]]] = { "Основные": [semantic_property("Вид", "Parameter", source="form_payload_parameter")], } if key_parameter is not None: add_grouped_property( groups, "Использование", semantic_property("KeyParameter", key_parameter, index=3, source="form_payload_parameter"), ) row: dict[str, Any] = { "category": "Parameter", "type_name": "Parameter", "name": name, "path": f"4.{index + 2}", "semantic": {"groups": groups}, } if include_parameters: row["parameters"] = direct_parameters(node, row["path"], limit=max_parameters) result.append(row) return result def event_handlers(tree: Any) -> list[dict[str, Any]]: base_path = "1.19" node = get_by_path(tree, base_path) if node is None or not children(node): for candidate_path in ("1.18", "1.27"): candidate = get_by_path(tree, candidate_path) candidate_items = children(candidate) if any( GUID_RE.fullmatch(scalar(candidate_items[index]) or "") and BSL_IDENTIFIER_RE.fullmatch(scalar(candidate_items[index + 1]) or "") for index in range(1, max(1, len(candidate_items) - 1)) ): base_path = candidate_path node = candidate break items = children(node) if not any( GUID_RE.fullmatch(scalar(items[index]) or "") and BSL_IDENTIFIER_RE.fullmatch(scalar(items[index + 1]) or "") for index in range(1, max(1, len(items) - 1)) ): form_node = get_by_path(tree, "1") for candidate_index, candidate in enumerate(children(form_node)): candidate_items = children(candidate) if any( GUID_RE.fullmatch(scalar(candidate_items[index]) or "") and BSL_IDENTIFIER_RE.fullmatch(scalar(candidate_items[index + 1]) or "") for index in range(1, max(1, len(candidate_items) - 1)) ): base_path = f"1.{candidate_index}" node = candidate break items = children(node) result: list[dict[str, Any]] = [] index = 1 while index + 1 < len(items): guid = scalar(items[index]) handler = scalar(items[index + 1]) if guid and handler and GUID_RE.fullmatch(guid) and BSL_IDENTIFIER_RE.fullmatch(handler): event_guid = guid.lower() if GUID_RE.fullmatch(guid) else None event_name = FORM_EVENT_NAMES.get(event_guid or "") result.append( { "name": event_name, "type_name": "Event", "event_name": FORM_EVENT_NAMES.get(event_guid or ""), "handler": handler, "guid": event_guid, "path": f"{base_path}.{index + 1}", } ) index += 2 return result def event_handlers_from_node(node: Any, base_path: str, *, owner: str | None = None, max_depth: int = 3) -> list[dict[str, Any]]: result: list[dict[str, Any]] = [] seen: set[tuple[str | None, str]] = set() def add_event(guid: str | None, handler: str, path: str) -> None: event_guid = guid.lower() if guid and GUID_RE.fullmatch(guid) else None key = (event_guid, handler.casefold()) if key in seen: return seen.add(key) result.append( { "kind": "element_event", **({"owner": owner} if owner else {}), "event_name": FORM_EVENT_NAMES.get(event_guid or ""), "handler": handler, "guid": event_guid, "path": path, } ) def walk(value: Any, path: str, depth: int) -> None: if depth > max_depth: return items = children(value) if depth > 0: marker = scalar(items[0]) if items else None if marker in FORM_ITEM_MARKERS: return if len(items) >= 3 and scalar(items[0]) and re.fullmatch(r"\d+", scalar(items[0]) or ""): index = 1 while index + 1 < len(items): guid = scalar(items[index]) handler = scalar(items[index + 1]) if guid and handler and GUID_RE.fullmatch(guid) and BSL_IDENTIFIER_RE.fullmatch(handler): add_event(guid, handler, path_join(path, index + 1)) index += 2 continue index += 1 for index, child in enumerate(items): walk(child, path_join(path, index), depth + 1) walk(node, base_path, 0) return result def form_item_name(node: Any, base_path: str) -> dict[str, str] | None: marker = child_scalar(node, 0) if marker == "22": index = 7 if not child_scalar(node, 5) and re.fullmatch(r"\d+", child_scalar(node, 6) or "") else 6 elif marker == "31": index = 5 elif marker == "34": direct = child_scalar(node, 5) index = 5 if direct and not re.fullmatch(r"-?\d+(?:\.\d+)?", direct) else 6 elif marker in {"35", "37"}: index = 7 if not child_scalar(node, 5) and re.fullmatch(r"\d+", child_scalar(node, 6) or "") else 6 elif marker == "48": index = 7 if children(child_at(node, 5)) else 6 elif marker == "55": index = 5 elif marker == "73": index = 5 if child_scalar(node, 5) and not re.fullmatch(r"-?\d+(?:\.\d+)?", child_scalar(node, 5) or "") else 6 else: index = 6 name = child_scalar(node, index) if name and not GUID_RE.fullmatch(name): return {"value": name, "path": path_join(base_path, index)} found = first_scalar_with_path(node, base_path, skip_guids=True) if found and found["value"] not in {"#", "Pattern", "B", "U", "S", "N", "D", "ru"}: return found return None def form_item_type_code(node: Any, marker: str | None) -> str | None: if marker == "6": return child_scalar(node, 5) if marker == "34": direct = child_scalar(node, 5) return child_scalar(node, 4) if direct and not re.fullmatch(r"-?\d+(?:\.\d+)?", direct or "") else direct if marker == "48": return child_scalar(node, 6) if children(child_at(node, 5)) else child_scalar(node, 5) if marker == "22": direct = child_scalar(node, 5) shifted = child_scalar(node, 6) if not direct and re.fullmatch(r"\d+", shifted or ""): return shifted return direct if marker in {"35", "37"}: direct = child_scalar(node, 5) shifted = child_scalar(node, 6) if not direct and re.fullmatch(r"\d+", shifted or ""): return shifted return direct if marker == "12": return child_scalar(node, 5) return child_scalar(node, 0) def embedded_table_addition_row( node: Any, path: str, depth: int, *, include_parameters: bool, max_parameters: int, ) -> dict[str, Any] | None: """Normalize a table addition stored as marker 5 inside a marker 55/73 tail.""" items = children(node) type_code = child_direct_scalar(node, 5) name = child_direct_scalar(node, 6) identity = children(items[1]) if len(items) > 1 else [] item_id = direct_scalar(identity[0]) if identity else None owner_guid = direct_scalar(identity[1]) if len(identity) > 1 else None if ( len(items) < 20 or child_direct_scalar(node, 0) != "5" or type_code not in FORM_TABLE_ADDITION_TYPE_NAMES or not name or not item_id or not re.fullmatch(r"\d+", item_id) or not owner_guid or not GUID_RE.fullmatch(owner_guid) ): return None evidence = collect_evidence(node) title = localized_text(items[7], path_join(path, 7)) if len(items) > 7 else None parameters = direct_parameters(node, path, roles=FORM_ITEM_PARAMETER_ROLES["6"], limit=max_parameters) row: dict[str, Any] = { "name": name, "name_path": path_join(path, 6), "id": item_id, "id_path": path_join(path_join(path, 1), 0), "title": title["value"] if title else None, "title_lang": title.get("lang") if title else None, "title_path": title["path"] if title else None, "path": path, "depth": depth, "marker": "6", "marker_name": "TableAddition", "type_code": type_code, "type_name": FORM_TABLE_ADDITION_TYPE_NAMES[type_code], "strings_sample": sorted(evidence["strings"])[:30], "guids_sample": sorted(evidence["guids"])[:20], } row["semantic"] = form_item_semantic_properties(row, parameters) enrich_table_addition_specific_semantics(row, node) row["semantic"] = public_semantic(row["semantic"], include_diagnostics=include_parameters) if include_parameters: row["parameters"] = parameters return row def item_records( tree: Any, *, limit: int = 500, include_parameters: bool = True, max_parameters: int = 80, ) -> tuple[list[dict[str, Any]], int, bool]: records: list[dict[str, Any]] = [] total = 0 def walk(node: Any, path: list[int], depth: int) -> None: nonlocal total items = children(node) marker = scalar(items[0]) if items else None if len(items) >= 6 and marker in FORM_ITEM_MARKERS: total += 1 current_path = ".".join(str(part) for part in path) name = form_item_name(node, current_path) if name and len(records) < limit: evidence = collect_evidence(node) title = localized_text(node, current_path) item_id = record_id(node, current_path) parameter_roles = FORM_ITEM_PARAMETER_ROLES.get(marker or "", {}) type_code = form_item_type_code(node, marker) public_type_name = form_item_public_type_name(marker, type_code, name["value"]) row = { "name": name["value"], "name_path": name["path"], "id": item_id["value"] if item_id else None, "id_path": item_id["path"] if item_id else None, "title": title["value"] if title else None, "title_lang": title.get("lang") if title else None, "title_path": title["path"] if title else None, "path": current_path, "depth": depth, "marker": marker, "marker_name": MARKER_NAMES.get(marker), "type_code": type_code, "type_name": public_type_name, "strings_sample": sorted(evidence["strings"])[:30], "guids_sample": sorted(evidence["guids"])[:20], } command_binding = form_button_command_binding(node, current_path) if command_binding: row["command_binding"] = command_binding command_reference = form_command_reference(node, current_path) if command_reference: row["command_reference"] = command_reference if command_reference.get("scope") == "form" and "command_binding" not in row: row["command_binding"] = command_reference data_path_index = 12 if marker in {"48", "73"} else 11 if marker in {"35", "37"} and str(row.get("name_path") or "").endswith(".7"): data_path_index = 12 if marker == "31": data_path_index = 9 if marker in {"31", "35", "37", "48", "55", "73"} and len(items) > data_path_index: data_path_reference = data_path_reference_from_node(items[data_path_index]) if data_path_reference.get("attribute_id"): row["data_path_attribute_id"] = data_path_reference["attribute_id"] if data_path_reference.get("field_id"): row["data_path_field_id"] = data_path_reference["field_id"] if data_path_reference: row["data_path_attribute_id_path"] = path_join(current_path, data_path_index) if marker in {"35", "37"} and len(items) > 12: footer_reference = footer_data_path_reference(items[11], items[12]) if footer_reference is not None: row["_footer_data_path_reference"] = footer_reference item_events = event_handlers_from_node(node, current_path, owner=name["value"], max_depth=3) if item_events: row["events"] = item_events parameters = direct_parameters(node, current_path, roles=parameter_roles, limit=max_parameters) row["semantic"] = form_item_semantic_properties(row, parameters) enrich_container_specific_semantics(row, node) enrich_table_addition_specific_semantics(row, node) enrich_input_field_specific_semantics(row, node) enrich_form_field_specific_semantics(row, node) enrich_dynamic_list_specific_semantics(row, node) enrich_table_specific_semantics(row, node) enrich_command_button_specific_semantics(row, node) enrich_decoration_specific_semantics(row, node) enrich_radio_button_specific_semantics(row, node) enrich_form_button_specific_semantics(row, node) row["semantic"] = public_semantic(row["semantic"], include_diagnostics=include_parameters) if include_parameters: row["parameters"] = parameters records.append(row) supports_embedded_additions = marker in {"55", "73"} or ( marker == "22" and str(row.get("type_name") or "") == "Командная панель" ) if supports_embedded_additions: for child_index, child in enumerate(items): addition = embedded_table_addition_row( child, path_join(current_path, child_index), depth + 1, include_parameters=include_parameters, max_parameters=max_parameters, ) if addition is None: continue total += 1 if len(records) < limit: records.append(addition) for index, child in enumerate(items): walk(child, [*path, index], depth + 1) root1 = get_by_path(tree, "1") walk(root1, [1], 0) enrich_item_reference_semantics(records) public_rows_semantics(records, include_diagnostics=include_parameters) return records[:limit], total, total > len(records) def section_record_total(tree: Any, path: str) -> int: node = get_by_path(tree, path) return len(declared_child_records(node, path)) if node is not None else 0 def section_records( tree: Any, path: str, category: str, *, limit: int = 500, include_parameters: bool = True, max_parameters: int = 80, ) -> list[dict[str, Any]]: node = get_by_path(tree, path) if node is None: return [] result = [] for record in declared_child_records(node, path)[:limit]: evidence = collect_evidence(record.node) name = record_name(record.node, category=category) title = localized_text(record.node, record.path) item_id = record_id(record.node, record.path) row = { "category": category, "index": record.index, "path": record.path, "name": name, "id": item_id["value"] if item_id else None, "id_path": item_id["path"] if item_id else None, "title": title["value"] if title else None, "title_lang": title.get("lang") if title else None, "title_path": title["path"] if title else None, "marker": child_scalar(record.node, 0), "marker_name": MARKER_NAMES.get(child_scalar(record.node, 0) or ""), "strings_sample": sorted(evidence["strings"])[:30], "guids_sample": sorted(evidence["guids"])[:20], } parameters = direct_parameters(record.node, record.path, roles=SECTION_RECORD_PARAMETER_ROLES, limit=max_parameters) row["semantic"] = section_record_semantic_properties(row, parameters, record.node) row["semantic"] = public_semantic(row["semantic"], include_diagnostics=include_parameters) if include_parameters: row["parameters"] = parameters if category == "Attribute": dynamic_fields = dynamic_list_fields(record.node, owner_name=name, owner_path=record.path) if dynamic_fields: row["dynamic_list_fields"] = dynamic_fields settings = dynamic_list_settings(record.node, owner_path=record.path) if settings: row["dynamic_list_settings"] = settings result.append(row) return result def attribute_by_id(attributes: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: return {str(item.get("id")): item for item in attributes if item.get("id") is not None} def dynamic_field_by_id(attributes: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for attribute in attributes: for field in attribute.get("dynamic_list_fields") or []: field_id = field.get("id") if field_id is None: continue result[str(field_id)] = field return result def dynamic_field_by_owner_and_id(attributes: list[dict[str, Any]]) -> dict[tuple[str, str], dict[str, Any]]: result: dict[tuple[str, str], dict[str, Any]] = {} for attribute in attributes: owner_id = attribute.get("id") if owner_id is None: continue for field in attribute.get("dynamic_list_fields") or []: field_id = field.get("id") if field_id is None: continue result[(str(owner_id), str(field_id))] = field return result def parameter_reference_id(parameter: dict[str, Any] | None) -> str | None: if not parameter: return None values = parameter.get("values_sample") or [] numbers = [str(item.get("value")) for item in values if str(item.get("value") or "").isdigit()] if numbers: return numbers[-1] value = parameter.get("value") return str(value) if str(value or "").isdigit() else None def enrich_item_data_paths(items: list[dict[str, Any]], attributes: list[dict[str, Any]]) -> None: by_id = attribute_by_id(attributes) by_name = {str(attribute.get("name") or ""): attribute for attribute in attributes if attribute.get("name")} dynamic_by_id = dynamic_field_by_id(attributes) dynamic_by_owner_and_id = dynamic_field_by_owner_and_id(attributes) table_items = [ item for item in items if str(item.get("marker") or "") in {"55", "73"} and item.get("name") and item.get("path") ] table_items.sort(key=lambda item: len(str(item.get("path") or "").split(".")), reverse=True) def fallback_data_path(item: dict[str, Any]) -> tuple[str | None, dict[str, Any] | None, str | None]: item_name = str(item.get("name") or "") if not item_name: return None, None, None attribute = by_name.get(item_name) if attribute is not None: return item_name, attribute, "form_attribute_name_match" item_path = str(item.get("path") or "") for table_item in table_items: table_path = str(table_item.get("path") or "") table_name = str(table_item.get("name") or "") if not table_path or not table_name or not item_path.startswith(table_path + ".") or not item_name.startswith(table_name): continue table_attribute = by_name.get(table_name) field_name = item_name[len(table_name) :] if not field_name: continue if table_attribute is not None: for field in table_attribute.get("dynamic_list_fields") or []: if field_name in {str(field.get("name") or ""), str(field.get("data_name") or "")}: return str(field.get("path_to_data") or f"{table_name}.{field_name}"), table_attribute, "tabular_attribute_field_name_match" object_attribute = by_name.get("Объект") if object_attribute is not None: public_field_name = FORM_PUBLIC_DATA_FIELD_NAMES.get(field_name, field_name) return f"Объект.{table_name}.{public_field_name}", object_attribute, "object_tabular_item_name_match" return None, None, None for item in items: marker = str(item.get("marker") or "") if marker not in {"31", "35", "37", "48", "55", "73"}: continue parameters = item.get("parameters") or [] data_path_index = 9 if marker == "31" else (12 if marker in {"48", "73"} else 11) reference = item.get("data_path_attribute_id") or parameter_reference_id(next((parameter for parameter in parameters if parameter.get("index") == data_path_index), None)) if not reference: continue field_id = item.get("data_path_field_id") attribute = by_id.get(reference) dynamic_field = dynamic_by_owner_and_id.get((str(reference), str(field_id))) if field_id else None fallback_source = None standard_field = FORM_STANDARD_DATA_FIELDS.get(str(field_id or "")) if standard_field and attribute: attribute_name = str(attribute.get("name") or "") if str(field_id) == "-2" and str(item.get("name") or "") == "Номер": standard_field = "Number" if str(field_id) == "-3" and str(item.get("name") or "") in {"Дата", "Date"}: standard_field = "Date" if attribute_name in {"Запись", "Record"} and str(item.get("name") or "") in {"Период", "Period"}: standard_field = "Period" is_dynamic_list = bool(attribute.get("dynamic_list_fields") or attribute.get("dynamic_list_settings")) path_to_data = ( f"Items.{attribute_name}.CurrentData.{standard_field}" if is_dynamic_list else f"{attribute_name}.{standard_field}" ) dynamic_field = None elif dynamic_field: dynamic_name = dynamic_field.get("data_name") or dynamic_field.get("name") if marker == "31" and attribute and attribute.get("dynamic_list_settings"): path_to_data = f"Items.{attribute.get('name')}.CurrentData.{dynamic_name}" else: path_to_data = dynamic_field.get("path_to_data") or dynamic_name elif attribute: attribute_name = str(attribute.get("name") or "") item_name = str(item.get("name") or "") item_path = str(item.get("path") or "") owner_table = next( ( table_item for table_item in table_items if table_item is not item and str(table_item.get("path") or "") and item_path.startswith(str(table_item.get("path")) + ".") ), None, ) if attribute_name != "Объект" and item_name.endswith("ДатаНачала"): path_to_data = f"{attribute_name}.StartDate" elif attribute_name != "Объект" and item_name.endswith("ДатаОкончания"): path_to_data = f"{attribute_name}.EndDate" elif attribute_name in {"Запись", "Record"} and item_name and item_name != attribute_name: path_to_data = f"{attribute_name}.{FORM_PUBLIC_DATA_FIELD_NAMES.get(item_name, item_name)}" elif attribute_name != "Объект": path_to_data = attribute_name elif owner_table is not None: table_name = str(owner_table.get("name") or "") field_name = item_name[len(table_name) :] if item_name.startswith(table_name) else item_name public_field_name = FORM_PUBLIC_DATA_FIELD_NAMES.get(field_name, field_name) path_to_data = f"{attribute_name}.{table_name}.{public_field_name}" elif item_name and item_name != attribute_name: public_field_name = FORM_PUBLIC_DATA_FIELD_NAMES.get(item_name, item_name) path_to_data = f"{attribute_name}.{public_field_name}" else: path_to_data = attribute_name elif not field_id and dynamic_by_id.get(reference): dynamic_field = dynamic_by_id.get(reference) path_to_data = dynamic_field.get("path_to_data") or dynamic_field.get("data_name") or dynamic_field.get("name") else: path_to_data, attribute, fallback_source = fallback_data_path(item) if not path_to_data: continue dynamic_field = None item["path_to_data"] = path_to_data if attribute and attribute.get("dynamic_list_settings"): item["dynamic_list_settings"] = attribute.get("dynamic_list_settings") semantic = item.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) add_grouped_property( groups, "Основные", semantic_property( "ПутьКДанным", path_to_data, source=fallback_source or ("standard_data_field_reference" if standard_field else ("dynamic_list_field_reference" if dynamic_field and not attribute else "form_attribute_reference")), ), ) for item in items: pending = item.pop("_type_link_reference", None) if not isinstance(pending, dict): continue owner_id = str(item.get("data_path_attribute_id") or "") field_id = str(pending.get("field_id") or "") attribute = by_id.get(owner_id) target = dynamic_by_owner_and_id.get((owner_id, field_id)) owner_name = str((attribute or {}).get("name") or "") field_name = str((target or {}).get("data_name") or (target or {}).get("name") or "") if not owner_name or not field_name: continue semantic = item.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) add_grouped_property( groups, "Использование", semantic_property( "TypeLink", { "path_to_data": f"Items.{owner_name}.CurrentData.{field_name}", "link_item": int(pending.get("link_item") or 0), }, index=42, source="form_payload_type_link_reference", ), ) mark_semantic_parameter_mapped(semantic, 42) for item in items: pending = item.pop("_footer_data_path_reference", None) if not isinstance(pending, dict) or pending.get("aggregate") != "Total": continue path_to_data = str(item.get("path_to_data") or "") if "." not in path_to_data and pending.get("primary_aggregate"): field_guid = str(pending.get("field_guid") or "") candidates = { str(candidate.get("path_to_data") or "") for candidate in items if candidate is not item and field_guid and field_guid in {str(value).lower() for value in candidate.get("guids_sample") or []} and "." in str(candidate.get("path_to_data") or "") } if candidates: deepest = max(candidate.count(".") for candidate in candidates) deepest_candidates = {candidate for candidate in candidates if candidate.count(".") == deepest} if len(deepest_candidates) == 1: path_to_data = deepest_candidates.pop() if "." not in path_to_data: continue owner_path, field_name = path_to_data.rsplit(".", 1) if not owner_path or not field_name: continue semantic = item.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) total_path = f"{owner_path}.Total{field_name}" property_name = "ПутьКДанным" if pending.get("primary_aggregate") else "FooterDataPath" if pending.get("primary_aggregate"): item["path_to_data"] = total_path add_grouped_property( groups, "Основные", semantic_property(property_name, total_path, index=12, source="form_payload_footer_data_reference"), ) mark_semantic_parameter_mapped(semantic, 12) def enrich_page_title_data_paths(items: list[dict[str, Any]]) -> None: """Resolve a page title counter from its single descendant table. Managed-form pages encode this through internal object-field GUIDs. The descendant table has already been resolved to a public data path, so the public ``RowsCount`` path can be derived without exposing storage IDs. """ tables = [ item for item in items if str(item.get("marker") or "") in {"55", "73"} and item.get("path_to_data") and item.get("path") ] for page in items: if str(page.get("type_name") or "") != "Страница": continue page_path = str(page.get("path") or "") if not page_path: continue descendant_tables = [ table for table in tables if str(table.get("path") or "").startswith(page_path + ".") ] if len(descendant_tables) != 1: continue table_path = str(descendant_tables[0].get("path_to_data") or "") if not table_path: continue semantic = page.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) add_grouped_property( groups, "Основные", semantic_property( "TitleDataPath", f"{table_path}.RowsCount", source="page_single_descendant_table", ), ) def enrich_item_event_links(items: list[dict[str, Any]], module: dict[str, Any] | None) -> None: names = routine_names(module) for item in items: events = item.get("events") or [] if not events: continue links = [] semantic = item.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) for event in events: handler = str(event.get("handler") or "") resolved = handler.casefold() in names event_presentation = str(event.get("event_name") or "") item_name = str(item.get("name") or "") if not event_presentation and item_name and handler.casefold().startswith(item_name.casefold()): event_presentation = handler[len(item_name) :] or "Обработчик" add_grouped_property( groups, "События", semantic_property(event_presentation or "Обработчик", handler, source="element_event_handler"), ) links.append( { "kind": "element_event", "element": item.get("name"), "event_name": event_presentation or event.get("event_name"), "handler": handler, "status": "resolved" if resolved else "missing", } ) item["handler_links"] = links def dynamic_list_fields(node: Any, *, owner_name: str | None, owner_path: str | None = None) -> list[dict[str, Any]]: values = atoms(node, limit=20000) fields: dict[str, dict[str, Any]] = {} index = 0 typed_prefixes = {"S", "N", "B", "U", "#"} while index < len(values) - 1: key = values[index] value_offset = 2 if values[index + 1] in typed_prefixes and index + 2 < len(values) else 1 value = values[index + value_offset] match = re.fullmatch(r"FieldsMapItem(Id|Name|SecondaryName)(\d+)", key or "") if match: field = fields.setdefault(match.group(2), {}) if match.group(1) == "Id": field["id"] = value elif match.group(1) == "Name": field["data_name"] = value elif match.group(1) == "SecondaryName": field["name"] = value index += 1 result = [] seen_ids: set[str] = set() for ordinal in sorted(fields, key=lambda item: int(item)): field = fields[ordinal] name = field.get("name") or field.get("data_name") if not name: continue data_name = field.get("data_name") field_id = field.get("id") if field_id is not None: seen_ids.add(str(field_id)) result.append( { "name": name, "data_name": data_name, "id": field_id, "path_to_data": f"{owner_name}.{data_name}" if owner_name and data_name else data_name, "ordinal": int(ordinal), "source": "dynamic_list_field_map", } ) for index, child in enumerate(children(node)): if child_scalar(child, 0) != "5": continue field_id = child_scalar(child, 1) name = child_scalar(child, 3) if not field_id or not name or str(field_id) in seen_ids: continue field_path = path_join(owner_path, index) if owner_path else None title = localized_text(child, field_path or "") result.append( { "name": name, "data_name": name, "id": field_id, "title": title.get("value") if title else None, "title_lang": title.get("lang") if title else None, "title_path": title.get("path") if title else None, **({"path": field_path} if field_path else {}), "path_to_data": f"{owner_name}.{name}" if owner_name and name else name, "ordinal": len(result), "source": "tabular_attribute_field", } ) seen_ids.add(str(field_id)) return result DYNAMIC_LIST_TYPED_PREFIXES = {"S", "N", "B", "U", "#"} DYNAMIC_LIST_SETTING_ALIASES = { "main_table": {"основнаятаблица", "maintable", "source_table", "sourcetable"}, "custom_query": {"произвольныйзапрос", "customquery", "arbitraryquery"}, "query_text": {"текстзапроса", "querytext", "запрос", "query"}, } def dynamic_list_bool(value: str | None) -> bool | str | None: if value == "1": return True if value == "0": return False return value if value not in {None, ""} else None def next_dynamic_list_value(entries: list[dict[str, Any]], index: int) -> dict[str, Any] | None: next_index = index + 1 if next_index < len(entries) and entries[next_index].get("value") in DYNAMIC_LIST_TYPED_PREFIXES: next_index += 1 if next_index < len(entries): return entries[next_index] return None def dynamic_list_settings(node: Any, *, owner_path: str | None = None) -> dict[str, Any] | None: entries = node_scalar_entries(node, owner_path or "", limit=30000) settings: dict[str, Any] = {} paths: dict[str, str] = {} sources: dict[str, str] = {} for index, entry in enumerate(entries): key = normalize_key(entry.get("value")) target = next((name for name, aliases in DYNAMIC_LIST_SETTING_ALIASES.items() if key in aliases), None) if not target: continue value_entry = next_dynamic_list_value(entries, index) if not value_entry: continue value = value_entry.get("value") settings[target] = dynamic_list_bool(value) if target == "custom_query" else value paths[target] = str(value_entry.get("path") or "") sources[target] = "key_value" if "query_text" not in settings: query_entry = next( ( entry for entry in entries if isinstance(entry.get("value"), str) and re.search(r"(?i)\bselect\b|выбрать", str(entry.get("value") or "")) ), None, ) if query_entry: settings["query_text"] = query_entry.get("value") paths["query_text"] = str(query_entry.get("path") or "") sources["query_text"] = "query_text_heuristic" if not settings: return None result = { "main_table": settings.get("main_table"), "custom_query": settings.get("custom_query"), "query_text": settings.get("query_text"), "paths": paths, "sources": sources, "status": "ok", } if settings.get("custom_query") is True and not settings.get("query_text"): result["status"] = "query_text_missing" return {key: value for key, value in result.items() if value is not None and value != "" and value != {} and value != []} def normalize_key(value: Any) -> str: return re.sub(r"[\s._-]+", "", str(value or "")).casefold() def dynamic_list_column_items(attributes: list[dict[str, Any]], *, limit: int = 5000) -> tuple[list[dict[str, Any]], int, bool]: records: list[dict[str, Any]] = [] total = 0 for attribute in attributes: owner_name = attribute.get("name") for field in attribute.get("dynamic_list_fields") or []: total += 1 if len(records) >= limit: continue name = field.get("name") path_to_data = field.get("path_to_data") semantic = { "groups": { "Основные": [ semantic_property("Идентификатор", field.get("id"), source="dynamic_list_field_map"), semantic_property("Имя", name, source="dynamic_list_field_map"), semantic_property("Заголовок", name, source="dynamic_list_field_map"), semantic_property("Вид", "Колонка динамического списка", source="dynamic_list_field_map"), semantic_property("ПутьКДанным", path_to_data, source="dynamic_list_field_map"), ] }, "coverage": {"mapped": 5, "unmapped": 0, "total": 5, "status": "ok"}, } records.append( { "name": name, "id": field.get("id"), "title": name, "marker": "dynamic_list_field", "marker_name": "DynamicListField", "type_code": "dynamic_list_field", "type_name": "Колонка динамического списка", "owner": owner_name, "data_name": field.get("data_name"), "path_to_data": path_to_data, "semantic": semantic, } ) return records, total, total > len(records) def additional_column_items(tree: Any, items: list[dict[str, Any]], *, limit: int = 5000) -> tuple[list[dict[str, Any]], int, bool]: """Decode managed-form AdditionalColumns stored after declared attributes.""" section = child_at(tree, 3) section_items = children(section) declared = child_direct_scalar(section, 1) declared_count = int(declared) if str(declared or "").isdigit() else 0 tail_start = min(len(section_items), 2 + declared_count) table_items = [ item for item in items if str(item.get("marker") or "") in {"55", "73"} and item.get("path_to_data") ] records: list[dict[str, Any]] = [] total = 0 for group_index, group in enumerate(section_items[tail_start:], start=tail_start): if child_direct_scalar(group, 0) != "0": continue group_items = children(group) count_text = child_direct_scalar(group, 2) column_count = int(count_text) if str(count_text or "").isdigit() else 0 owner_guids = guids(child_at(group, 1), limit=4) owner_guid = owner_guids[-1] if owner_guids else "" owners = [ item for item in table_items if owner_guid and owner_guid in {str(value).lower() for value in item.get("guids_sample") or []} ] if len(owners) != 1: continue owner = owners[0] owner_path = str(owner.get("path_to_data") or "") for offset, column in enumerate(group_items[3 : 3 + column_count], start=3): if child_direct_scalar(column, 0) != "5": continue name = child_direct_scalar(column, 3) if not name: continue total += 1 if len(records) >= limit: continue column_id = child_direct_scalar(column, 1) path_to_data = f"{owner_path}.{name}" records.append( { "name": name, "id": column_id, "marker": "additional_column", "marker_name": "AdditionalColumn", "type_code": "additional_column", "type_name": "Колонка реквизита", "owner": owner.get("name"), "owner_guid": owner_guid, "path": f"3.{group_index}.{offset}", "path_to_data": path_to_data, "semantic": { "groups": { "Основные": [ semantic_property("Идентификатор", column_id, source="form_payload_additional_column"), semantic_property("Имя", name, source="form_payload_additional_column"), semantic_property("Вид", "Колонка реквизита", source="form_payload_additional_column"), semantic_property("ПутьКДанным", path_to_data, source="form_payload_additional_column"), ] }, "coverage": {"mapped": 4, "unmapped": 0, "total": 4, "status": "ok"}, }, } ) return records, total, total > len(records) def form_common_parameters(tree: Any, *, limit: int = 120) -> list[dict[str, Any]]: node = get_by_path(tree, "1") if node is None: return [] parameters = direct_parameters( node, "1", roles={ 0: "Версия/тип формы", 1: "Основные свойства формы", 27: "События формы", }, limit=limit, ) for parameter in parameters: index = parameter.get("index") typed_node = child_at(node, index) if isinstance(index, int) else None typed_marker = child_direct_scalar(typed_node, 0) if typed_marker == "#": parameter["typed_guid"] = child_direct_scalar(typed_node, 1) parameter["typed_value"] = child_direct_scalar(typed_node, 2) parameter["typed_kind"] = "enum" elif typed_marker == "B": parameter["typed_value"] = child_direct_scalar(typed_node, 1) parameter["typed_kind"] = "boolean" return parameters def form_common_semantic(parameters: list[dict[str, Any]], *, include_diagnostics: bool = True) -> dict[str, Any]: by_index = {int(item["index"]): item for item in parameters if isinstance(item, dict) and isinstance(item.get("index"), int)} form_version = str((by_index.get(0) or {}).get("value") or "") auto_save_raw = str((by_index.get(7) or {}).get("value") or "") auto_save_data_in_settings = {"0": "DontUse", "1": "Use"}.get(auto_save_raw) group_indices = [11, 40, 47, 57] group_values = [str((by_index.get(index) or {}).get("value") or "") for index in group_indices] form_group = None if group_values == ["0", "0", "0", "0"]: form_group = "Vertical" elif group_values == ["1", "1", "1", "1"]: form_group = "Horizontal" elif group_values == ["1", "1", "3", "3"]: form_group = "AlwaysHorizontal" elif group_values == ["1", "2", "2", "2"]: form_group = "HorizontalIfPossible" elif form_version in {"49", "50"} and group_values[0] == "0": form_group = "Vertical" window_primary = str((by_index.get(2) or {}).get("value") or "") window_companion = str((by_index.get(54) or {}).get("value") or "") window_opening_mode = None if (window_primary, window_companion) == ("0", "0"): window_opening_mode = "DontBlock" elif (window_primary, window_companion) == ("1", "1"): window_opening_mode = "LockOwner" elif (window_primary, window_companion) == ("2", "2"): window_opening_mode = "LockWholeInterface" elif form_version in {"49", "50"}: window_opening_mode = {"0": "DontBlock", "1": "LockOwner"}.get(window_primary) show_primary = str((by_index.get(17) or {}).get("value") or "") show_companion = str((by_index.get(56) or {}).get("value") or "") show_command_bar = None if (show_primary, show_companion) == ("0", "0"): show_command_bar = False elif show_companion == "1" and show_primary in {"2", "3"}: show_command_bar = True command_bar_location = { ("0", "0"): "None", ("2", "1"): "Top", ("3", "1"): "Bottom", }.get((show_primary, show_companion)) if form_version in {"49", "50"} and show_primary == "0": show_command_bar = False command_bar_location = "None" auto_title = bool_presentation(str((by_index.get(9) or {}).get("value") or "")) if form_version in {"49", "50"} else None vertical_scroll = "useIfNecessary" if form_version == "49" and str((by_index.get(36) or {}).get("value") or "") == "2" else None use_for_raw = str((by_index.get(20) or {}).get("typed_value") or "") use_for_guid = str((by_index.get(20) or {}).get("typed_guid") or "") use_for_folders_and_items = choice_folders_and_items_presentation(use_for_raw) if use_for_guid == "59ef2b80-c86b-11d5-a3c1-0050bae0a776" else None auto_time = "CurrentOrLast" if use_for_guid == "adeb08a0-415c-11d6-b9d1-0050bae0a95d" and use_for_raw == "3" else None posting_parameter = by_index.get(22) or {} use_posting_mode = ( "Auto" if str(posting_parameter.get("typed_guid") or "") == "20d89b09-bd04-4304-a8c7-4d07fac6338a" and str(posting_parameter.get("typed_value") or "") == "3" else None ) repost_parameter = by_index.get(24) or {} repost_on_write = bool_presentation(str(repost_parameter.get("typed_value") or "")) if repost_parameter.get("typed_kind") == "boolean" else None mapped_indexes: set[int] = set() groups: dict[str, list[dict[str, Any]]] = {} if form_group is not None: mapped_indexes.update(group_indices) groups.setdefault("Основные", []).append( { "name": "Группировка", "value": form_group, "source": "controlled_designer_form_root_group", "status": "ok", "parameter_indices": group_indices, "parameter_values": group_values, "write_shape": "composite_scalar", } ) if auto_save_data_in_settings is not None: mapped_indexes.add(7) groups.setdefault("Основные", []).append( { "name": "АвтоСохранениеДанныхВНастройках", "value": auto_save_data_in_settings, "source": "controlled_designer_form_auto_save_data_in_settings", "status": "ok", "parameter_indices": [7], "parameter_values": [auto_save_raw], "write_shape": "scalar_enum", } ) if window_opening_mode is not None: mapped_indexes.update({2, 54}) groups.setdefault("Основные", []).append( { "name": "РежимОткрытияОкна", "value": window_opening_mode, "source": "controlled_designer_form_window_opening_mode", "status": "ok", "parameter_indices": [2, 54], "parameter_values": [window_primary, window_companion], "write_shape": "paired_scalar", } ) if show_command_bar is not None: mapped_indexes.update({17, 56}) groups.setdefault("Основные", []).append( { "name": "ОтображатьКоманднуюПанель", "value": show_command_bar, "source": "controlled_designer_form_show_command_bar", "status": "ok", "parameter_indices": [17, 56], "parameter_values": [show_primary, show_companion], "write_shape": "paired_scalar", } ) if command_bar_location is not None: mapped_indexes.update({17, 56}) groups.setdefault("Основные", []).append( { "name": "ПоложениеКоманднойПанели", "value": command_bar_location, "source": "controlled_designer_form_command_bar_location", "status": "ok", "parameter_indices": [17, 56], "parameter_values": [show_primary, show_companion], "write_shape": "paired_scalar_shared", } ) if use_for_folders_and_items is not None: mapped_indexes.add(20) groups.setdefault("Основные", []).append( { "name": "UseForFoldersAndItems", "value": use_for_folders_and_items, "source": "live_sql_typed_form_enum", "status": "ok", "parameter_index": 20, "parameter_value": use_for_raw, "write_shape": "typed_enum_read_only", } ) if auto_title is not None: mapped_indexes.add(9) groups.setdefault("Прочее", []).append( { "name": "AutoTitle", "value": auto_title, "source": "form_payload_root_versioned", "status": "ok", "parameter_index": 9, "write_shape": "scalar_boolean_read_only", } ) if vertical_scroll is not None: mapped_indexes.add(36) groups.setdefault("Прочее", []).append( { "name": "VerticalScroll", "value": vertical_scroll, "source": "form_payload_root_versioned", "status": "ok", "parameter_index": 36, "write_shape": "scalar_enum_read_only", } ) for name, value, index, source in ( ("AutoTime", auto_time, 20, "live_sql_typed_form_enum"), ("UsePostingMode", use_posting_mode, 22, "live_sql_typed_form_enum"), ("RepostOnWrite", repost_on_write, 24, "live_sql_typed_form_boolean"), ): if value is None: continue mapped_indexes.add(index) groups.setdefault("Прочее", []).append( { "name": name, "value": value, "source": source, "status": "ok", "parameter_index": index, "write_shape": "typed_read_only", } ) semantic = { "groups": groups, "coverage": semantic_coverage(parameters, mapped_indexes), "unmapped_parameters": semantic_unmapped_parameters(parameters, mapped_indexes), } return public_semantic(semantic, include_diagnostics=include_diagnostics) def enrich_form_common_semantic(form_semantic: dict[str, Any], items: list[dict[str, Any]]) -> None: command_bar = next( ( item for item in items if isinstance(item, dict) and str(item.get("id") or "") == "-1" and str(item.get("type_name") or "") == "Командная панель" and item.get("name") ), None, ) if command_bar is None: return groups = form_semantic.setdefault("groups", {}) add_grouped_property( groups, "Основные", semantic_property( "АвтоКоманднаяПанель", command_bar.get("name"), source="form_item_reference:id=-1", ), ) def module_summary(tree: Any, *, include_text: bool = False) -> dict[str, Any] | None: text = scalar(get_by_path(tree, "2")) if not text: return None routines = [{"kind": match.group(1), "name": match.group(2)} for match in BSL_ROUTINE_RE.finditer(text)] result: dict[str, Any] = { "path": "2", "bytes_estimate": len(text.encode("utf-8")), "chars": len(text), "routine_count": len(routines), "routine_names": [str(item.get("name") or "") for item in routines if item.get("name")], "routines_sample": routines[:80], "text_preview": text[:500], } if include_text: result["text"] = text return result def routine_names(module: dict[str, Any] | None) -> set[str]: summary = module or {} names = {str(value or "").casefold() for value in summary.get("routine_names") or [] if value} names.update( str(item.get("name") or "").casefold() for item in summary.get("routines_sample") or [] if isinstance(item, dict) and item.get("name") ) return names def handler_links(events: list[dict[str, Any]], module: dict[str, Any] | None) -> list[dict[str, Any]]: names = routine_names(module) links = [] for event in events: handler = str(event.get("handler") or "") links.append( { "kind": "form_event", "event_name": event.get("event_name"), "handler": handler, "status": "resolved" if handler.casefold() in names else "missing", "event_path": event.get("path"), } ) return links def command_handler_links(commands: list[dict[str, Any]], module: dict[str, Any] | None) -> list[dict[str, Any]]: names = routine_names(module) links = [] for command in commands: handler = str(command.get("action") or command.get("name") or "") if not handler: continue resolved = handler.casefold() in names action_is_explicit = command.get("action_source") == "form_payload_parameter_8" status = "resolved" if resolved else "missing" if action_is_explicit else "no_handler_expected" links.append( { "kind": "form_command", "command": command.get("name"), "handler": handler, "status": status, "match_by": "action" if action_is_explicit else "command_name", **( {} if resolved else { "diagnostics": { "message": ( "Явный обработчик Action не найден в декодированном модуле формы." if action_is_explicit else "Явный BSL-обработчик с именем команды не найден; команда может быть платформенной или декларативной." ) } } ), "command_path": command.get("path"), "command_guid": (command.get("guids_sample") or [None])[0], } ) return links def button_command_links(items: list[dict[str, Any]], commands: list[dict[str, Any]]) -> list[dict[str, Any]]: command_by_guid = {} command_by_id = {str(command.get("id") or ""): command for command in commands if command.get("id") is not None} for command in commands: for guid in command.get("guids_sample") or []: command_by_guid[str(guid).lower()] = command links = [] for item in items: if item.get("marker") not in {"31", "34"}: continue binding = item.get("command_binding") if isinstance(item.get("command_binding"), dict) else None if binding and binding.get("command_name"): links.append( { "kind": "command_button", "button": item.get("name"), "command": binding.get("command_name"), "command_name": binding.get("command_name"), "status": "resolved", "match_by": binding.get("match_by"), "button_path": item.get("path"), "command_path": binding.get("path"), "command_guid": binding.get("group_guid"), "command_binding": binding, } ) continue if binding and binding.get("scope") == "form": command = command_by_id.get(str(binding.get("command_id") or "")) if command is not None: links.append( { "kind": "command_button", "button": item.get("name"), "command": command.get("name"), "command_name": f"Form.Command.{command.get('name')}", "status": "resolved", "match_by": binding.get("match_by"), "button_path": item.get("path"), "command_path": command.get("path"), "command_guid": binding.get("group_guid"), "command_binding": binding, } ) continue command_reference = item.get("command_reference") if isinstance(item.get("command_reference"), dict) else None if command_reference: reference_key = (str(command_reference.get("guid") or "").lower(), str(command_reference.get("code") or "")) suffix = FORM_GRAPHICAL_SCHEMA_STANDARD_COMMANDS.get( reference_key ) graphical_fields = [candidate for candidate in items if candidate.get("type_name") == "GraphicalSchemaField"] if suffix and len(graphical_fields) == 1: owner_name = str(graphical_fields[0].get("name") or "") command_name = f"Form.Item.{owner_name}.StandardCommand.{suffix}" links.append( { "kind": "command_button", "button": item.get("name"), "command": command_name, "command_name": command_name, "status": "resolved", "match_by": "graphical_schema_standard_command_guid", "button_path": item.get("path"), "command_path": command_reference.get("path"), "command_guid": command_reference.get("guid"), } ) continue suffix = FORM_TABLE_STANDARD_COMMANDS.get(reference_key[0]) table_fields = [ candidate for candidate in items if candidate.get("type_name") in {"Динамический список", "Таблица формы"} and str(candidate.get("id") or "") == reference_key[1] ] if suffix and len(table_fields) == 1: owner_name = str(table_fields[0].get("name") or "") command_name = f"Form.Item.{owner_name}.StandardCommand.{suffix}" links.append( { "kind": "command_button", "button": item.get("name"), "command": command_name, "command_name": command_name, "status": "resolved", "match_by": "table_standard_command_guid", "button_path": item.get("path"), "command_path": command_reference.get("path"), "command_guid": command_reference.get("guid"), } ) continue object_command = FORM_OBJECT_COMMANDS.get(reference_key) query_texts = [ str((candidate.get("dynamic_list_settings") or {}).get("query_text") or "") for candidate in items if isinstance(candidate.get("dynamic_list_settings"), dict) ] public_refs = { f"Task.{match.group(1)}" for query_text in query_texts for match in re.finditer(r"(?:Задача|Task)\.([A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*)", query_text) } if object_command and len(public_refs) == 1: command_name = f"{next(iter(public_refs))}.Command.{object_command}" links.append( { "kind": "command_button", "button": item.get("name"), "command": command_name, "command_name": command_name, "status": "resolved", "match_by": "object_command_guid_and_dynamic_query", "button_path": item.get("path"), "command_path": command_reference.get("path"), "command_guid": command_reference.get("guid"), } ) continue if object_command: item_name = str(item.get("name") or "") name_match = re.search( rf"(?:Задача|Task)([A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*?){re.escape(object_command)}$", item_name, ) if name_match: command_name = f"Task.{name_match.group(1)}.Command.{object_command}" links.append( { "kind": "command_button", "button": item.get("name"), "command": command_name, "command_name": command_name, "status": "resolved", "match_by": "object_command_guid_and_button_name", "button_path": item.get("path"), "command_path": command_reference.get("path"), "command_guid": command_reference.get("guid"), } ) continue matched_guid = next((guid for guid in item.get("guids_sample") or [] if str(guid).lower() in command_by_guid), None) button_text = " ".join(str(value or "") for value in [item.get("name"), item.get("title"), *(item.get("strings_sample") or [])]).casefold() command = command_by_guid[str(matched_guid).lower()] if matched_guid else None match_by = "guid" if command is None: item_name = str(item.get("name") or "").casefold() for candidate in sorted(commands, key=lambda value: len(str(value.get("name") or "")), reverse=True): candidate_name = str(candidate.get("name") or "").casefold() if candidate_name and (item_name.endswith(candidate_name) or candidate_name in item_name): command = candidate match_by = "name" break if command is None: continue command_names = [str(value or "") for value in [command.get("name"), command.get("title"), command.get("id")]] if match_by == "guid" and not any(value and value.casefold() in button_text for value in command_names): command = None item_name = str(item.get("name") or "").casefold() for candidate in sorted(commands, key=lambda value: len(str(value.get("name") or "")), reverse=True): candidate_name = str(candidate.get("name") or "").casefold() if candidate_name and (item_name.endswith(candidate_name) or candidate_name in item_name): command = candidate match_by = "name" matched_guid = None break if command is None: continue links.append( { "kind": "command_button", "button": item.get("name"), "command": command.get("name"), "command_name": f"Form.Command.{command.get('name')}", "status": "resolved", "match_by": match_by, "button_path": item.get("path"), "command_path": command.get("path"), "command_guid": matched_guid, } ) return links def enrich_button_command_semantics(items: list[dict[str, Any]], links: list[dict[str, Any]]) -> None: command_by_button = {str(link.get("button") or ""): link for link in links if link.get("button") and (link.get("command") or link.get("command_name"))} for item in items: link = command_by_button.get(str(item.get("name") or "")) if not link: continue semantic = item.setdefault("semantic", {}) groups = semantic.setdefault("groups", {}) add_grouped_property( groups, "Основные", semantic_property("ИмяКоманды", link.get("command_name") or f"Form.Command.{link.get('command')}", source=f"button_command_link:{link.get('match_by') or 'unknown'}"), ) def decode_form_payload( tree: Any, *, max_items: int = 500, include_module_text: bool = False, include_parameters: bool = True, max_parameters: int = 80, ) -> dict[str, Any]: root = root_signature(tree) items, items_total, items_truncated = item_records(tree, limit=max_items, include_parameters=include_parameters, max_parameters=max_parameters) attributes = section_records(tree, "3", "Attribute", limit=max_items, include_parameters=include_parameters, max_parameters=max_parameters) parameters = form_parameter_rows(tree, include_parameters=include_parameters, max_parameters=max_parameters) enrich_item_data_paths(items, attributes) enrich_page_title_data_paths(items) additional_items, additional_items_total, additional_items_truncated = additional_column_items( tree, items, limit=max(0, max_items - len(items)), ) if additional_items: items.extend(additional_items) items_total += additional_items_total items_truncated = items_truncated or additional_items_truncated or items_total > len(items) dynamic_items, dynamic_items_total, dynamic_items_truncated = dynamic_list_column_items(attributes, limit=max(0, max_items - len(items))) if dynamic_items: items.extend(dynamic_items) items_total += dynamic_items_total items_truncated = items_truncated or dynamic_items_truncated or items_total > len(items) commands = section_records(tree, "5", "Command", limit=max_items, include_parameters=include_parameters, max_parameters=max_parameters) tables = section_records(tree, "6", "Table", limit=max_items, include_parameters=include_parameters, max_parameters=max_parameters) command_bars = section_records(tree, "7", "CommandBar", limit=max_items, include_parameters=include_parameters, max_parameters=max_parameters) totals = { "items": items_total, "attributes": section_record_total(tree, "3"), "commands": section_record_total(tree, "5"), "tables": section_record_total(tree, "6"), "command_bars": section_record_total(tree, "7"), } module = module_summary(tree, include_text=include_module_text) enrich_item_event_links(items, module) events = event_handlers(tree) event_links = handler_links(events, module) link_items, _, _ = item_records(tree, limit=max(items_total, max_items), include_parameters=False) link_commands = section_records(tree, "5", "Command", limit=max(totals["commands"], max_items), include_parameters=False) command_links = command_handler_links(link_commands, module) button_links = button_command_links(link_items, link_commands) enrich_button_command_semantics(items, button_links) public_rows_semantics(items, include_diagnostics=include_parameters) public_rows_semantics(attributes, include_diagnostics=include_parameters) public_rows_semantics(parameters, include_diagnostics=include_parameters) public_rows_semantics(commands, include_diagnostics=include_parameters) public_rows_semantics(tables, include_diagnostics=include_parameters) public_rows_semantics(command_bars, include_diagnostics=include_parameters) form_parameters = form_common_parameters(tree, limit=max_parameters) form_semantic = form_common_semantic(form_parameters, include_diagnostics=include_parameters) enrich_form_common_semantic(form_semantic, items) result = { "schema": "onec_form_payload_profile.v1", "status": "ok" if root.get("root_marker") == "4" else "not_form_payload", "root": root, "form_semantic": form_semantic, **({"form_parameters": form_parameters} if include_parameters else {}), "events": events, "items": items, "attributes": attributes, "parameters": parameters, "commands": commands, "tables": tables, "command_bars": command_bars, "module": module, "handler_links": event_links, "command_links": command_links, "button_command_links": button_links, "counts": {}, } result["counts"] = { "events": len(result["events"]), "items": len(result["items"]), "items_total": totals["items"], "attributes": len(result["attributes"]), "parameters": len(result["parameters"]), "attributes_total": totals["attributes"], "commands": len(result["commands"]), "commands_total": totals["commands"], "tables": len(result["tables"]), "tables_total": totals["tables"], "command_bars": len(result["command_bars"]), "command_bars_total": totals["command_bars"], "module_routines": ((result.get("module") or {}).get("routine_count") or 0), "handler_links": len(result["handler_links"]), "resolved_handlers": sum(1 for item in result["handler_links"] if item.get("status") == "resolved"), "missing_handlers": sum(1 for item in result["handler_links"] if item.get("status") == "missing"), "command_links": len(result["command_links"]), "resolved_commands": sum(1 for item in result["command_links"] if item.get("status") == "resolved"), "missing_commands": sum(1 for item in result["command_links"] if item.get("status") == "missing"), "button_command_links": len(result["button_command_links"]), "parameters_included": bool(include_parameters), "max_parameters": max_parameters if include_parameters else 0, "items_truncated": items_truncated, "attributes_truncated": totals["attributes"] > len(result["attributes"]), "commands_truncated": totals["commands"] > len(result["commands"]), "tables_truncated": totals["tables"] > len(result["tables"]), "command_bars_truncated": totals["command_bars"] > len(result["command_bars"]), } return result