Files
2026-08-14 09:40:51 +03:00

4000 lines
185 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)
value = parameter_value(parameters, index)
source = "form_payload"
# A managed-form record can store the localized title outside its
# direct parameter #3. The row decoder already resolves that exact
# title path, so expose it instead of misleading an agent with an
# empty semantic «Заголовок» beside a non-empty public row.title.
if index == 3 and value in {None, ""} and row.get("title") not in {None, ""}:
value = row.get("title")
source = "form_payload_title_path"
add_grouped_property(groups, group, semantic_property(name, value, index=index, source=source))
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'}"),
)
FORM_AUXILIARY_ITEM_TYPES = {
"Контекстное меню", "Расширенная подсказка", "SearchStringAddition",
"ViewStatusAddition", "SearchControlAddition",
}
def form_item_coverage_summary(items: list[dict[str, Any]]) -> dict[str, Any]:
"""Report semantic coverage without auxiliary form records hiding control quality."""
buckets = {
"all_items": {"items": 0, "mapped": 0, "unmapped": 0},
"interactive_items": {"items": 0, "mapped": 0, "unmapped": 0},
"auxiliary_items": {"items": 0, "mapped": 0, "unmapped": 0},
}
for item in items:
if not isinstance(item, dict):
continue
coverage = (item.get("semantic") or {}).get("coverage") if isinstance(item.get("semantic"), dict) else None
if not isinstance(coverage, dict):
continue
target = "auxiliary_items" if str(item.get("type_name") or "") in FORM_AUXILIARY_ITEM_TYPES else "interactive_items"
for bucket_name in ("all_items", target):
bucket = buckets[bucket_name]
bucket["items"] += 1
bucket["mapped"] += int(coverage.get("mapped") or 0)
bucket["unmapped"] += int(coverage.get("unmapped") or 0)
for bucket in buckets.values():
bucket["total"] = bucket["mapped"] + bucket["unmapped"]
bucket["status"] = "partial" if bucket["unmapped"] else "ok"
return buckets
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)
coverage_summary = form_item_coverage_summary(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,
"item_coverage": coverage_summary,
**({"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