diff --git a/.claude/skills/form-decompile/scripts/form-decompile.py b/.claude/skills/form-decompile/scripts/form-decompile.py new file mode 100644 index 00000000..ec3a0f42 --- /dev/null +++ b/.claude/skills/form-decompile/scripts/form-decompile.py @@ -0,0 +1,3623 @@ +#!/usr/bin/env python3 +# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. +# +# Зеркало form-decompile.ps1 (канон). Структура 1:1 — те же имена функций, порядок, +# комментарии. Изменения вносить сначала в .ps1, затем переносить сюда строка-в-строку. +import argparse +import os +import re +import sys +import xml.etree.ElementTree as ET +from collections import OrderedDict +from decimal import Decimal + +# --- 1. Namespaces --- +NS_LF = "http://v8.1c.ru/8.3/xcf/logform" +NS_V8 = "http://v8.1c.ru/8.1/data/core" +NS_XR = "http://v8.1c.ru/8.3/xcf/readable" +NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" + +NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings" +NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema" +NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core" +NS_V8UI = "http://v8.1c.ru/8.1/data/ui" +NS_APP = "http://v8.1c.ru/8.2/managed-application/core" + +# Карта префиксов для ET.find/findall (зеркало XmlNamespaceManager). +NS = { + 'lf': NS_LF, + 'v8': NS_V8, + 'xr': NS_XR, + 'xsi': NS_XSI, + 'dcsset': NS_DCSSET, + 'dcssch': NS_DCSSCH, + 'dcscor': NS_DCSCOR, + 'v8ui': NS_V8UI, + 'app': NS_APP, +} + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе, +# компилятор регенерит тот же скелет → чистый раундтрип. +CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + +# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей. +# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы +# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions). +COMPANION_TAGS = ['ContextMenu', 'ExtendedTooltip', 'AutoCommandBar'] + +# Скрипт-скоуп состояние (зеркало $script:* канона). ROOT/NS_DOC ставятся в main(). +ROOT = None +NSMAP_DOC = {} +OUTPUT_DIR = None +OUTPUT_BASENAME = None +QUERY_FILES_ACCUMULATOR = [] +QUERY_FILE_NAMES_USED = {} + +# Атрибут с пространством имён в формате ET ({uri}local) — зеркало GetAttribute("x", $NS_XSI). +_XSI_TYPE = '{%s}type' % NS_XSI + + +def _attr(node, name, ns_uri=None): + """GetAttribute(name[, ns]) — .NET возвращает "" для отсутствующего атрибута, ET → None.""" + if node is None: + return '' + key = ('{%s}%s' % (ns_uri, name)) if ns_uri else name + v = node.get(key) + return v if v is not None else '' + + +# ═══════════════════════════════════════════════════════════════════════════ +# JSON-сериализатор — точное зеркало ConvertTo-CompactJson/Try-InlineJson/ +# Convert-StringToJsonLiteral. json.dumps НЕ подходит (иная раскладка inline/ +# multiline и эскейпинг). Кириллица в выводе сырая (UTF-8 без эскейпа). +# ═══════════════════════════════════════════════════════════════════════════ + +def convert_string_to_json_literal(s): + if s is None: + return 'null' + sb = ['"'] + for ch in s: + code = ord(ch) + if code == 0x22: + sb.append('\\"') + elif code == 0x5C: + sb.append('\\\\') + elif code == 0x08: + sb.append('\\b') + elif code == 0x09: + sb.append('\\t') + elif code == 0x0A: + sb.append('\\n') + elif code == 0x0C: + sb.append('\\f') + elif code == 0x0D: + sb.append('\\r') + elif code < 0x20: + sb.append('\\u%04x' % code) + else: + sb.append(ch) + sb.append('"') + return ''.join(sb) + + +def _num_to_str(obj): + """Зеркало [System.Convert]::ToString(.., InvariantCulture).""" + if isinstance(obj, bool): + return 'true' if obj else 'false' + if isinstance(obj, int): + return str(obj) + # Decimal — зеркало [decimal].ToString(): сохраняет масштаб ("1.50" остаётся "1.50"). + if isinstance(obj, Decimal): + return str(obj) + # float — зеркало [double].ToString('R'/InvariantCulture): кратчайшее round-trip, без хвостового .0. + r = repr(float(obj)) + if r.endswith('.0'): + r = r[:-2] + return r + + +def try_inline_json(obj): + if obj is None: + return 'null' + if isinstance(obj, bool): + return 'true' if obj else 'false' + if isinstance(obj, str): + return convert_string_to_json_literal(obj) + if isinstance(obj, (int, float, Decimal)): + return _num_to_str(obj) + if isinstance(obj, dict): + if len(obj) == 0: + return '{}' + parts = [] + for k in obj.keys(): + v = try_inline_json(obj[k]) + if v is None: + return None + parts.append('%s: %s' % (convert_string_to_json_literal(str(k)), v)) + return '{ ' + ', '.join(parts) + ' }' + if isinstance(obj, (list, tuple)): + items = list(obj) + if len(items) == 0: + return '[]' + parts = [] + for it in items: + v = try_inline_json(it) + if v is None: + return None + parts.append(v) + return '[' + ', '.join(parts) + ']' + return None + + +def convert_to_compact_json(obj, depth=0, indent_unit=' ', line_limit=120): + indent = indent_unit * depth + child_indent = indent_unit * (depth + 1) + if obj is None: + return 'null' + if isinstance(obj, bool): + return 'true' if obj else 'false' + if isinstance(obj, str): + return convert_string_to_json_literal(obj) + if isinstance(obj, (int, float, Decimal)): + return _num_to_str(obj) + is_container = isinstance(obj, (dict, list, tuple)) + if is_container: + inline_attempt = try_inline_json(obj) + if inline_attempt is not None and (len(indent) + len(inline_attempt)) <= line_limit: + return inline_attempt + if isinstance(obj, dict): + keys = list(obj.keys()) + if len(keys) == 0: + return '{}' + parts = [] + for k in keys: + val = convert_to_compact_json(obj[k], depth + 1, indent_unit, line_limit) + parts.append('%s%s: %s' % (child_indent, convert_string_to_json_literal(str(k)), val)) + return '{\n' + ',\n'.join(parts) + '\n' + indent + '}' + if isinstance(obj, (list, tuple)): + items = list(obj) + if len(items) == 0: + return '[]' + parts = ['%s%s' % (child_indent, convert_to_compact_json(it, depth + 1, indent_unit, line_limit)) for it in items] + return '[\n' + ',\n'.join(parts) + '\n' + indent + ']' + return convert_string_to_json_literal(str(obj)) + + +# ═══════════════════════════════════════════════════════════════════════════ +# Низкоуровневые читатели текста. +# .NET .InnerText = конкатенация текста всех потомков. PreserveWhitespace=false +# в каноне сворачивает whitespace-only текст-узлы в "". ET не имеет такого режима, +# поэтому два читателя на одном ws-сохраняющем дереве: +# _text — main-doc семантика (whitespace-only → ""), для всех чтений значений; +# _text_ws — сырой текст (для Resolve-WS: восстановление точного числа пробелов). +# ═══════════════════════════════════════════════════════════════════════════ + +def _text(node): + if node is None: + return None + s = ''.join(node.itertext()) + if s != '' and s.strip() == '': + return '' + return s + + +def _text_ws(node): + if node is None: + return None + return ''.join(node.itertext()) + + +# Запрос ≥3 строк + есть outputDir → вынести в `-.sql`, вернуть "@file". +def maybe_externalize_query(query_text, list_name): + if not query_text: + return query_text + if not OUTPUT_DIR: + return query_text + line_count = len(re.findall('\n', query_text)) + 1 + if line_count < 3: + return query_text + safe = re.sub(r'[^\w\-]', '_', list_name) + if not safe: + safe = 'query' + prefix = (OUTPUT_BASENAME + '-') if OUTPUT_BASENAME else '' + file_name = prefix + safe + '.sql' + suffix = 1 + while file_name in QUERY_FILE_NAMES_USED: + suffix += 1 + file_name = '%s%s_%d.sql' % (prefix, safe, suffix) + QUERY_FILE_NAMES_USED[file_name] = True + QUERY_FILES_ACCUMULATOR.append(OrderedDict([('fileName', file_name), ('text', query_text)])) + return '@' + file_name + + +def save_query_files(): + if len(QUERY_FILES_ACCUMULATOR) == 0: + return + if not OUTPUT_DIR: + return + for qf in QUERY_FILES_ACCUMULATOR: + with open(os.path.join(OUTPUT_DIR, qf['fileName']), 'w', encoding='utf-8', newline='') as f: + f.write(qf['text']) + sys.stderr.write("Saved %d external query file(s)\n" % len(QUERY_FILES_ACCUMULATOR)) + + +# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/ +# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false. +def test_list_settings_has_content(ls_node): + if ls_node is None: + return False + for cont in ('filter', 'order', 'conditionalAppearance', 'dataParameters'): + cn = ls_node.find('dcsset:%s' % cont, NS) + if cn is not None and cn.find('dcsset:item', NS) is not None: + return True + return False + + +# Форма ListSettings: ordered-карта present top-level элементов. $null, если форма == +# полному каноничному скелету ИЛИ содержит неподдержанные top-level элементы → канон-fallback. +def get_list_settings_shape(ls_node, has_grouping=False): + if ls_node is None: + return None + shape = OrderedDict() + for child in list(ls_node): + tag = _local_name(child.tag) + if tag in ('filter', 'order', 'conditionalAppearance'): + has_vm = child.find('dcsset:viewMode', NS) is not None + has_us = child.find('dcsset:userSettingID', NS) is not None + code = ('v' if has_vm else '') + ('u' if has_us else '') + usp_node = child.find('dcsset:userSettingPresentation', NS) + if usp_node is not None: + usp = get_pres_by_type(usp_node) + shape[tag] = OrderedDict([('meta', code), ('presentation', usp)]) + else: + shape[tag] = code + elif tag == 'itemsViewMode': + shape['itemsViewMode'] = True + elif tag == 'itemsUserSettingID': + shape['itemsUserSettingID'] = True + elif tag == 'itemsUserSettingPresentation': + shape['itemsUserSettingPresentation'] = get_pres_by_type(child) + elif tag == 'dataParameters': + shape['dataParameters'] = True + elif tag == 'item': + if has_grouping: + shape['structure'] = True + else: + return None + else: + return None + if (len(shape) == 5 and shape.get('filter') == 'vu' and shape.get('order') == 'vu' + and shape.get('conditionalAppearance') == 'vu' and shape.get('itemsViewMode') is True + and shape.get('itemsUserSettingID') is True): + return None + return shape + + +# Группировка строк динамического списка: цепочка . +# Плоский массив уровней ИЛИ $null (не «чистая линейная цепочка одно-польных уровней»). +def build_group_level(fn): + field = get_child(fn, 'field') + gt = get_child(fn, 'groupType') + pat = get_child(fn, 'periodAdditionType') + pab_n = fn.find('dcsset:periodAdditionBegin', NS) + pae_n = fn.find('dcsset:periodAdditionEnd', NS) + pab = None + pae = None + if pab_n is not None: + pt = _attr(pab_n, 'type', NS_XSI) + pv = _text(pab_n) + if re.search(r'Field$', pt) or (pv and pv != '0001-01-01T00:00:00'): + pab = pv + if pae_n is not None: + pt = _attr(pae_n, 'type', NS_XSI) + pv = _text(pae_n) + if re.search(r'Field$', pt) or (pv and pv != '0001-01-01T00:00:00'): + pae = pv + is_default = ((not gt) or gt == 'Items') and ((not pat) or pat == 'None') and (not pab) and (not pae) + if is_default: + return field + o = OrderedDict([('field', field)]) + if gt and gt != 'Items': + o['groupType'] = gt + if pat and pat != 'None': + o['periodAdditionType'] = pat + if pab: + o['periodAdditionBegin'] = pab + if pae: + o['periodAdditionEnd'] = pae + return o + + +def build_list_grouping(item_node): + levels = [] + cur = item_node + while cur is not None: + if not re.search(r'StructureItemGroup$', _attr(cur, 'type', NS_XSI)): + return None + gi = None + nested = [] + for ch in list(cur): + ln = _local_name(ch.tag) + if ln == 'groupItems': + if gi is not None: + return None + gi = ch + elif ln == 'item': + nested.append(ch) + else: + return None + if gi is None: + return None + field_items = gi.findall('dcsset:item', NS) + if len(field_items) != 1: + return None + fn = field_items[0] + if not re.search(r'GroupItemField$', _attr(fn, 'type', NS_XSI)): + return None + levels.append(build_group_level(fn)) + if len(nested) == 0: + break + if len(nested) > 1: + return None + cur = nested[0] + if len(levels) == 0: + return None + return levels + + +# Ring-3: конструкции вне зоны поддержки → stderr + exit 3 (см. ring3-скан в main). +def fail_ring3(kind, loc): + sys.stderr.write("form-decompile: декомпиляция пока не поддерживает %s (path: %s)\n" % (kind, loc)) + sys.stderr.write("Для точечной работы с этой формой используй /form-edit.\n") + sys.exit(3) + + +# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...} +def get_lang_text(node): + if node is None: + return None + items = node.findall('v8:item', NS) + if len(items) == 0: + return None + m = OrderedDict() + for it in items: + lang = it.find('v8:lang', NS) + content = it.find('v8:content', NS) + if lang is not None: + m[_text(lang)] = _text(content) if content is not None else "" + if len(m) == 1 and 'ru' in m: + return m['ru'] + return m + + +# Точное число пробелов whitespace-only : ET хранит .text дословно, +# второй WS-парс не нужен — читаем сырой текст того же узла (_text_ws). +def resolve_ws(content_node): + if content_node is None: + return None + return _text_ws(content_node) + + +# Точное восстановление пробела: whitespace-only content → реальная строка пробелов. +def restore_ws_content(content_node): + ws = resolve_ws(content_node) + if ws and ws.strip() == '': + return ws + return ' ' + + +def get_lang_text_ws(node): + t = get_lang_text(node) + if t is None: + return None + if isinstance(t, str): + cn = node.find('v8:item/v8:content', NS) + if t == '' and cn is not None: + return restore_ws_content(cn) + return t + for it in node.findall('v8:item', NS): + lang = it.find('v8:lang', NS) + content = it.find('v8:content', NS) + if lang is not None and content is not None: + lt = _text(lang) + if lt in t and t[lt] == '': + t[lt] = restore_ws_content(content) + return t + + +# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile. +def title_from_name(name): + if not name: + return '' + s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) + s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) + parts = s.split(' ') + if len(parts) == 0: + return s + out = [parts[0]] + for i in range(1, len(parts)): + p = parts[i] + if len(p) > 1 and p == p.upper(): + out.append(p) + else: + out.append(p.lower()) + return ' '.join(out) + + +# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!). +FMT_MARKUP_RE = r'|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' + + +def test_has_real_markup(text): + if text is None: + return False + vals = list(text.values()) if isinstance(text, dict) else [str(text)] + for v in vals: + if re.search(FMT_MARKUP_RE, str(v)): + return True + return False + + +# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа или явный {text, formatted}. +def get_ml_formatted_value(title_node): + if title_node is None: + return None + text = get_lang_text_ws(title_node) + if text is None: + return None + fmt_attr = (_attr(title_node, 'formatted') == 'true') + if fmt_attr == test_has_real_markup(text): + return text + o = OrderedDict() + o['text'] = text + o['formatted'] = fmt_attr + return o + + +# Прочитать дочерний скаляр (по local-name, без namespace) +def get_child(node, name): + if node is None: + return None + c = node.find('{*}%s' % name) + if c is not None: + return _text(c) + return None + + +def has_child(node, name): + if node is None: + return False + return node.find('{*}%s' % name) is not None + + +def to_bool(v): + return v == 'true' + + +# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка). +def convert_typed_value(raw, xsi_type): + if re.search(r'decimal$', xsi_type): + if re.match(r'^-?\d+$', raw): + return int(raw) + return float(raw) + if re.search(r'boolean$', xsi_type): + return raw == 'true' + return raw + + +# Прочитать дочерний скаляр по xpath (с NS). Аналог skd Get-Text. +def get_text(node, xpath): + if node is None: + return None + if xpath is None or xpath == '': + return _text(node) + n = node.find(xpath, NS) + if n is not None: + return _text(n) + return None + + +# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash. Алиас Get-LangText. +def get_ml_text(node): + return get_lang_text(node) + + +# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string. +def get_pres_text(node): + if node is None: + return None + ml = get_ml_text(node) + if ml is not None: + return ml + t = _text(node) + if t: + return t + return None + + +# Presentation, сохраняющий ФОРМУ по xsi:type (ru-only LocalStringType ≠ xs:string). +def get_pres_by_type(node): + if node is None: + return None + xt = _attr(node, 'type', NS_XSI) + if re.search(r'LocalStringType$', xt): + d = OrderedDict() + for it in node.findall('v8:item', NS): + lang = get_text(it, 'v8:lang') + content = get_text(it, 'v8:content') + if lang: + d[lang] = content + if len(d) > 0: + return d + return None + t = _text(node) + if t: + return t + return None + + +# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo") +def get_local_xsi_type(node): + if node is None: + return None + t = _attr(node, 'type', NS_XSI) + mt = re.search(r':(.+)$', t) + if mt: + return mt.group(1) + return t + + +# Зеркало булевой коэрции PowerShell `if ($x)`: одноэлементный массив коэрсится в truthiness +# СВОЕГО элемента (а не «непустой список → true», как в Python). Нужно там, где канон пишет +# `if ($arr)` на результате билдера: напр. вырожденный FunctionalOptions @("") → falsy → дроп. +def _ps_truthy(v): + if v is None: + return False + if isinstance(v, bool): + return v + if isinstance(v, str): + return v != '' + if isinstance(v, (int, float)): + return v != 0 + if isinstance(v, dict): + return True # Hashtable/[ordered] — объект, всегда truthy + if isinstance(v, (list, tuple)): + if len(v) == 0: + return False + if len(v) == 1: + return _ps_truthy(v[0]) + return True + return True + + +# Зеркало PowerShell -eq/-ne для строк: регистронезависимое сравнение (PS по умолчанию ignore-case). +# Нужно там, где сравниваются произвольно-регистровые строки (заголовок vs авто-вывод из имени). +def _ps_ieq(a, b): + return a.lower() == b.lower() + + +# Зеркало PowerShell "$v" (строковая интерполяция): bool→True/False, float без хвостового .0, +# $null→'', иначе str(). Используется везде, где канон делает "$value". +def _ps_str(v): + if isinstance(v, bool): + return 'True' if v else 'False' + if isinstance(v, (float, Decimal)): + return _num_to_str(v) + if v is None: + return '' + return str(v) + + +# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile). +def get_font_value(val_node): + f = OrderedDict([('@type', 'Font')]) + for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + a = val_node.get(attr_name) + if a is not None: + f[attr_name] = a + return f + + +# Линия (граница) оформления → объект {@type:Line, width, gap, style}. +def get_line_value(val_node): + obj = OrderedDict([('@type', 'Line')]) + w = _attr(val_node, 'width') + g = _attr(val_node, 'gap') + if w != '': + obj['width'] = int(w) if re.match(r'^-?\d+$', w) else w + if g != '': + obj['gap'] = (g == 'true') + style_node = val_node.find('v8ui:style', NS) + if style_node is not None: + obj['style'] = _text(style_node) + return obj + + +# Прочитать в JSON-значение: Font/Line/Field/multilang/raw text. +def read_appearance_value_node(val_node): + if val_node is None: + return None + vt = get_local_xsi_type(val_node) + if vt == 'LocalStringType': + # НЕ схлопываем одноязычный в строку: значение параметра оформления различает + # xs:string (плоская строка) и LocalStringType (локализуемый текст). + m = OrderedDict() + for it in val_node.findall('v8:item', NS): + lang = it.find('v8:lang', NS) + content = it.find('v8:content', NS) + if lang is not None: + m[_text(lang)] = _text(content) if content is not None else "" + return m + if vt == 'Font': + return get_font_value(val_node) + if vt == 'Line': + return get_line_value(val_node) + if vt == 'Field': + return OrderedDict([('field', _text(val_node))]) + return _text(val_node) + + +# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd). +FILTER_OP_MAP = { + 'Equal': '=', 'NotEqual': '<>', 'Greater': '>', 'GreaterOrEqual': '>=', + 'Less': '<', 'LessOrEqual': '<=', 'InList': 'in', 'NotInList': 'notIn', + 'InHierarchy': 'inHierarchy', 'InListByHierarchy': 'inListByHierarchy', + 'Contains': 'contains', 'NotContains': 'notContains', + 'BeginsWith': 'beginsWith', 'NotBeginsWith': 'notBeginsWith', + 'Like': 'like', 'NotLike': 'notLike', + 'Filled': 'filled', 'NotFilled': 'notFilled', +} + +# Авто-детект DTV-ссылки (Перечисление.X/Catalog.X/…): компилятор сам выводит DesignTimeValue +# для таких значений → не фиксируем явный valueType. Инлайн в каноне (дважды) — держим как константу. +_DTV_REF_RE = r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' + + +# Render filter value node → shorthand-acceptable scalar string +def get_filter_value(val_node): + if val_node is None: + return '_' + nil = _attr(val_node, 'nil', NS_XSI) + if nil == 'true': + return '_' + v_type = get_local_xsi_type(val_node) + if v_type == 'DesignTimeValue': + return _text(val_node) + if v_type == 'LocalStringType': + return get_ml_text(val_node) + txt = _text(val_node) + if not txt: + return '_' + return txt + + +# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field). +def get_filter_value_with_type(val_node): + if val_node is None: + return {'value': '_', 'type': None} + raw_type = _attr(val_node, 'type', NS_XSI) + nil = _attr(val_node, 'nil', NS_XSI) + if nil == 'true': + return {'value': '_', 'type': None} + v_type = get_local_xsi_type(val_node) + if v_type == 'LocalStringType': + return {'value': get_ml_text(val_node), 'type': raw_type} + # Стандартная дата начала/окончания: SBD Custom+date → голая ISO-дата без valueType; + # именованный вариант → строка + valueType; SED Custom/нетипичное → {variant, date} + valueType. + if v_type == 'StandardBeginningDate' or v_type == 'StandardEndDate': + variant_n = val_node.find('v8:variant', NS) + date_n = val_node.find('v8:date', NS) + variant_str = _text(variant_n) if variant_n is not None else '' + if date_n is not None: + if v_type == 'StandardBeginningDate' and variant_str == 'Custom': + return {'value': _text(date_n), 'type': None} + return {'value': OrderedDict([('variant', variant_str), ('date', _text(date_n))]), 'type': raw_type} + return {'value': variant_str, 'type': raw_type} + txt = _text(val_node) + if not txt: + return {'value': '_', 'type': raw_type} + if v_type == 'boolean': + return {'value': (txt == 'true'), 'type': raw_type} + if v_type == 'decimal': + if re.match(r'^-?\d+$', txt): + return {'value': int(txt), 'type': raw_type} + return {'value': float(txt), 'type': raw_type} + return {'value': txt, 'type': raw_type} + + +# Convert filter item node → shorthand string или object form (рекурсивно для групп). +def build_filter_item(item_node, loc): + xtype = get_local_xsi_type(item_node) + if xtype == 'FilterItemGroup': + gt = get_text(item_node, 'dcsset:groupType') + group_name = {'OrGroup': 'Or', 'NotGroup': 'Not'}.get(gt, 'And') + items = [] + for c in item_node.findall('dcsset:item', NS): + bi = build_filter_item(c, '%s/item' % loc) + if bi is not None: + items.append(bi) + g_obj = OrderedDict([('group', group_name), ('items', items)]) + if get_text(item_node, 'dcsset:use') == 'false': + g_obj['use'] = False # группа отключена (@off) + g_pres_node = item_node.find('dcsset:presentation', NS) + if g_pres_node is not None: + g_pres = get_pres_by_type(g_pres_node) + if g_pres is not None and g_pres != '': + g_obj['presentation'] = g_pres + g_vm_node = item_node.find('dcsset:viewMode', NS) + if g_vm_node is not None: + g_obj['viewMode'] = _text(g_vm_node) + g_usid = get_text(item_node, 'dcsset:userSettingID') + if g_usid: + g_obj['userSettingID'] = 'auto' + g_uspn = item_node.find('dcsset:userSettingPresentation', NS) + if g_uspn is not None: + g_usp = get_pres_text(g_uspn) + if g_usp: + g_obj['userSettingPresentation'] = g_usp + return g_obj + if xtype != 'FilterItemComparison': + sys.stderr.write("form-decompile: пропущен фильтр неизвестного типа '%s' (path: %s)\n" % (xtype, loc)) + return None + left_node = item_node.find('dcsset:left', NS) + field = _text(left_node) if left_node is not None else None + ct = get_text(item_node, 'dcsset:comparisonType') + op = FILTER_OP_MAP.get(ct) + if not op: + op = ct + + right_nodes = item_node.findall('dcsset:right', NS) + value = None + value_is_array_flag = False + value_type_attr = None + if len(right_nodes) == 1: + rn = right_nodes[0] + if get_local_xsi_type(rn) == 'ValueListType': + value = [] + value_is_array_flag = True + else: + vt = get_filter_value_with_type(rn) + value = vt['value'] + auto_detects_dtv = (vt['type'] == 'dcscor:DesignTimeValue') and \ + bool(re.search(_DTV_REF_RE, _ps_str(vt['value']))) + if vt['type'] and not re.match(r'^xs:', vt['type']) and not auto_detects_dtv: + value_type_attr = vt['type'] + elif vt['type'] == 'xs:string' and isinstance(value, str) and re.search(r'^(-?\d+(\.\d+)?|\d{4}-\d{2}-\d{2}T)', value): + # Значение-строка "1"/"2020-..." с xs:string: компилятор авто-детектит число/дату → фиксируем явный valueType. + value_type_attr = 'xs:string' + elif len(right_nodes) > 1: + arr = [] + raw_types = [] + for rn in right_nodes: + arr.append(get_filter_value(rn)) + raw_types.append(_attr(rn, 'type', NS_XSI)) + value = arr + value_is_array_flag = True + uniq_types = sorted(set(raw_types)) + if len(uniq_types) == 1 and uniq_types[0]: + auto_detects_dtv = (uniq_types[0] == 'dcscor:DesignTimeValue') and \ + (len(arr) > 0) and \ + (len([x for x in arr if not re.search(_DTV_REF_RE, _ps_str(x))]) == 0) + if not auto_detects_dtv: + value_type_attr = uniq_types[0] + + use = get_text(item_node, 'dcsset:use') + user_id = get_text(item_node, 'dcsset:userSettingID') + vm_node = item_node.find('dcsset:viewMode', NS) + view_mode = _text(vm_node) if vm_node is not None else None + user_pres_node = item_node.find('dcsset:userSettingPresentation', NS) + fi_pres_node = item_node.find('dcsset:presentation', NS) + fi_pres = None + if fi_pres_node is not None: + fi_pres = get_pres_by_type(fi_pres_node) + + flags = [] + if use == 'false': + flags.append('@off') + if user_id: + flags.append('@user') + if view_mode == 'QuickAccess': + flags.append('@quickAccess') + elif view_mode == 'Inaccessible': + flags.append('@inaccessible') + elif view_mode == 'Normal': + flags.append('@normal') + + no_value_ops = ('filled', 'notFilled') + + # Пустой xs:string right ≠ отсутствие ; пробельные значения рвут shorthand → форсим объект. + val_needs_obj = False + if len(right_nodes) == 1 and not value_is_array_flag and op not in no_value_ops: + if _ps_str(value) == '_': + ws = resolve_ws(right_nodes[0]) + if ws and len(ws) > 0 and ws.strip() == '': + value = ws + val_needs_obj = True + elif value is not None and re.search(r'\s', _ps_str(value)): + val_needs_obj = True + + if user_pres_node is not None or value_is_array_flag or value_type_attr or fi_pres or val_needs_obj: + obj = OrderedDict([('field', field), ('op', op)]) + if op not in no_value_ops and value is not None: + if value_is_array_flag: + arr_as_list = [] + for vv in (value if isinstance(value, (list, tuple)) else [value]): + arr_as_list.append(vv) + obj['value'] = arr_as_list + else: + obj['value'] = value + if value_type_attr: + obj['valueType'] = value_type_attr + if use == 'false': + obj['use'] = False + if user_id: + obj['userSettingID'] = 'auto' + if fi_pres: + obj['presentation'] = fi_pres + if view_mode: + obj['viewMode'] = view_mode + if user_pres_node is not None: + obj['userSettingPresentation'] = get_pres_text(user_pres_node) + return obj + + s = field if field is not None else '' # $null + строка в PS → '' (избегаем TypeError) + if op in no_value_ops: + s += ' ' + op + else: + v_display = '_' + if value is not None: + if isinstance(value, bool): + v_display = 'true' if value else 'false' + elif _ps_str(value) != '': + v_display = _ps_str(value) + s += ' ' + op + ' ' + v_display + if flags: + s += ' ' + ' '.join(flags) + return s + + +# Рекурсивный хелпер одного элемента selection (для conditionalAppearance). +def build_selection_item(item, loc): + xt = get_local_xsi_type(item) + if not xt: + f_name = get_text(item, 'dcsset:field') + if f_name: + return f_name + field_el = item.find('dcsset:field', NS) + if field_el is not None: + return 'Auto' + if xt == 'SelectedItemAuto': + use_v = get_text(item, 'dcsset:use') + if use_v == 'false': + return OrderedDict([('auto', True), ('use', False)]) + return 'Auto' + if xt == 'SelectedItemField': + f_name = get_text(item, 'dcsset:field') + title_node = item.find('dcsset:lwsTitle', NS) + title = get_ml_text(title_node) + vm_n = item.find('dcsset:viewMode', NS) + use_v = get_text(item, 'dcsset:use') + use_false = (use_v == 'false') + if title or vm_n is not None or use_false: + obj = OrderedDict([('field', f_name)]) + if use_false: + obj['use'] = False + if title: + obj['title'] = title + if vm_n is not None: + obj['viewMode'] = _text(vm_n) + return obj + return f_name + if xt == 'SelectedItemFolder': + title_node = item.find('dcsset:lwsTitle', NS) + folder_title = get_ml_text(title_node) + inner = [] + for sub in item.findall('dcsset:item', NS): + bi = build_selection_item(sub, '%s/folder' % loc) + if bi is not None: + inner.append(bi) + entry = OrderedDict([('folder', folder_title), ('items', inner)]) + folder_field = get_text(item, 'dcsset:field') + if folder_field: + entry['field'] = folder_field + pl_n = item.find('dcsset:placement', NS) + if pl_n is not None and _text(pl_n) and _text(pl_n) != 'Auto': + entry['placement'] = _text(pl_n) + return entry + sys.stderr.write("form-decompile: пропущен элемент selection неизвестного типа '%s' (path: %s)\n" % (xt, loc)) + return None + + +# Build selection items array (для conditionalAppearance). +def build_selection(sel_node, loc): + if sel_node is None: + return [] + out = [] + for it in sel_node.findall('dcsset:item', NS): + bi = build_selection_item(it, loc) + if bi is not None: + out.append(bi) + return out + + +# Build order items array. +def build_order(ord_node, loc): + if ord_node is None: + return [] + out = [] + for it in ord_node.findall('dcsset:item', NS): + xt = get_local_xsi_type(it) + if xt == 'OrderItemAuto': + out.append('Auto') + elif xt == 'OrderItemField': + fn = get_text(it, 'dcsset:field') + ot = get_text(it, 'dcsset:orderType') + vm_n = it.find('dcsset:viewMode', NS) + use_v = get_text(it, 'dcsset:use') + use_false = (use_v == 'false') + if vm_n is not None or use_false: + obj = OrderedDict([('field', fn)]) + if use_false: + obj['use'] = False + if ot == 'Desc': + obj['direction'] = 'desc' + if vm_n is not None: + obj['viewMode'] = _text(vm_n) + out.append(obj) + else: + if ot == 'Desc': + out.append('%s desc' % fn) + else: + out.append(fn) + else: + sys.stderr.write("form-decompile: пропущен элемент сортировки неизвестного типа '%s' (path: %s)\n" % (xt, loc)) + return out + + +# Build appearance dict из (Line/Font/multilang/nested items). +def get_settings_appearance(app_node): + if app_node is None: + return None + d = OrderedDict() + for it in app_node.findall('dcscor:item', NS): + p_name = get_text(it, 'dcscor:parameter') + val = it.find('dcscor:value', NS) + if not p_name or val is None: + continue + raw_val = read_appearance_value_node(val) + use_v = get_text(it, 'dcscor:use') + nested_items = OrderedDict() + for sub in it.findall('dcscor:item', NS): + sub_name = get_text(sub, 'dcscor:parameter') + sub_val = sub.find('dcscor:value', NS) + if not sub_name: + continue + sub_raw = read_appearance_value_node(sub_val) + sub_use = get_text(sub, 'dcscor:use') + sub_entry = OrderedDict([('value', sub_raw)]) + if sub_use == 'false': + sub_entry['use'] = False + nested_items[sub_name] = sub_entry + val_is_line = isinstance(raw_val, dict) and ('@type' in raw_val) and (raw_val['@type'] == 'Line') + if val_is_line: + if use_v == 'false': + raw_val['use'] = False + if len(nested_items) > 0: + raw_val['items'] = nested_items + d[p_name] = raw_val + elif (use_v == 'false') or (len(nested_items) > 0): + wrap = OrderedDict([('value', raw_val)]) + if use_v == 'false': + wrap['use'] = False + if len(nested_items) > 0: + wrap['items'] = nested_items + d[p_name] = wrap + else: + d[p_name] = raw_val + return d + + +# Build conditionalAppearance array. +def build_conditional_appearance(ca_node, loc): + if ca_node is None: + return [] + out = [] + i = 0 + for it in ca_node.findall('dcsset:item', NS): + entry = OrderedDict() + scope_node = it.find('dcsset:scope', NS) + if scope_node is not None and (len(scope_node) > 0 or bool(_text(scope_node))): + sys.stderr.write("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: %s/%d/scope)\n" % (loc, i)) + sel_node = it.find('dcsset:selection', NS) + if sel_node is not None and len(sel_node.findall('dcsset:item', NS)) > 0: + entry['selection'] = build_selection(sel_node, '%s/%d/selection' % (loc, i)) + filter_node = it.find('dcsset:filter', NS) + if filter_node is not None and len(filter_node.findall('dcsset:item', NS)) > 0: + f = [] + for fc in filter_node.findall('dcsset:item', NS): + bi = build_filter_item(fc, '%s/%d/filter' % (loc, i)) + if bi is not None: + f.append(bi) + entry['filter'] = f + app_node = it.find('dcsset:appearance', NS) + ap = get_settings_appearance(app_node) + if ap and len(ap) > 0: + entry['appearance'] = ap + pres_node = it.find('dcsset:presentation', NS) + if pres_node is not None: + pres = get_pres_by_type(pres_node) + if pres is not None and pres != '': + entry['presentation'] = pres + vm_n = it.find('dcsset:viewMode', NS) + if vm_n is not None: + entry['viewMode'] = _text(vm_n) + usid = get_text(it, 'dcsset:userSettingID') + if usid: + entry['userSettingID'] = 'auto' + usp_n = it.find('dcsset:userSettingPresentation', NS) + if usp_n is not None: + usp = get_pres_text(usp_n) + if usp: + entry['userSettingPresentation'] = usp + use_v = get_text(it, 'dcsset:use') + if use_v == 'false': + entry['use'] = False + use_in_dont_use = [] + for ch in list(it): + if not ch.tag.startswith('{%s}' % NS_DCSSET): + continue + ln = _local_name(ch.tag) + mt = re.match(r'^useIn(.+)$', ln) + if mt and _text(ch) == 'DontUse': + short_name = mt.group(1)[0:1].lower() + mt.group(1)[1:] + use_in_dont_use.append(short_name) + if len(use_in_dont_use) > 0: + entry['useInDontUse'] = use_in_dont_use + out.append(entry) + i += 1 + return out + + +# Общие layout-свойства → в obj (симметрично Emit-Layout компилятора). +def add_layout(obj, node): + if get_child(node, 'DefaultItem') == 'true': + obj['defaultItem'] = True + soi = get_child(node, 'SkipOnInput') + if soi is not None: + obj['skipOnInput'] = (soi == 'true') + esd = get_child(node, 'EnableStartDrag') + if esd is not None: + obj['enableStartDrag'] = (esd == 'true') + edr = get_child(node, 'EnableDrag') + if edr is not None: + obj['enableDrag'] = (edr == 'true') + fdm = get_child(node, 'FileDragMode') + if fdm: + obj['fileDragMode'] = fdm + # AutoMaxWidth: компилятор додумывает false для multiLine-input → захват факт. значения. + amw_node = get_child(node, 'AutoMaxWidth') + if amw_node == 'false': + obj['autoMaxWidth'] = False + elif amw_node == 'true': + obj['autoMaxWidth'] = True + elif get_child(node, 'MultiLine') == 'true': + obj['autoMaxWidth'] = True + mw = get_child(node, 'MaxWidth') + if mw: + obj['maxWidth'] = int(mw) + if get_child(node, 'AutoMaxHeight') == 'false': + obj['autoMaxHeight'] = False + mh = get_child(node, 'MaxHeight') + if mh: + obj['maxHeight'] = int(mh) + w = get_child(node, 'Width') + if w: + obj['width'] = int(w) + h = get_child(node, 'Height') + if h: + obj['height'] = int(h) + hs = get_child(node, 'HorizontalStretch') + if hs is not None: + obj['horizontalStretch'] = (hs == 'true') + vs = get_child(node, 'VerticalStretch') + if vs is not None: + obj['verticalStretch'] = (vs == 'true') + gha = get_child(node, 'GroupHorizontalAlign') + if gha: + obj['groupHorizontalAlign'] = gha + gva = get_child(node, 'GroupVerticalAlign') + if gva: + obj['groupVerticalAlign'] = gva + ha = get_child(node, 'HorizontalAlign') + if ha: + obj['horizontalAlign'] = ha + for p in ('ShowInHeader', 'ShowInFooter', 'AutoCellHeight'): + v = get_child(node, p) + if v is not None: + obj[p[0:1].lower() + p[1:]] = (v == 'true') + fha = get_child(node, 'FooterHorizontalAlign') + if fha: + obj['footerHorizontalAlign'] = fha + hha = get_child(node, 'HeaderHorizontalAlign') + if hha: + obj['headerHorizontalAlign'] = hha + hdp = get_child(node, 'HeaderDataPath') + if hdp: + obj['headerDataPath'] = hdp + hf_node = node.find('lf:HeaderFormat', NS) + if hf_node is not None: + hf = get_lang_text(hf_node) + if hf is not None: + obj['headerFormat'] = hf + + +# TitleLocation у check/radio: тега нет → ""; значение = умный дефолт → опускаем; иначе пишем. +def add_title_location(obj, node, smart_default): + tl = get_child(node, 'TitleLocation') + if tl is None: + obj['titleLocation'] = '' + elif tl != smart_default: + obj['titleLocation'] = tl.lower() + + +# Разобрать элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика }. +def get_events(node, el_name): + ev = node.find('lf:Events', NS) + if ev is None: + return None + events = OrderedDict() + for e in ev.findall('lf:Event', NS): + events[_attr(e, 'name')] = _text(e) + if len(events) == 0: + return None + return events + + +# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use). +def decompile_xr_flag(node, tag): + el = node.find('{*}%s' % tag) + if el is None: + return None + common_node = el.find('{*}Common') + common = (common_node is not None and _text(common_node) == 'true') + val_nodes = el.findall('{*}Value') + if len(val_nodes) == 0: + return common + roles = OrderedDict() + for v in val_nodes: + rn = _attr(v, 'name') + if re.match(r'^Role\.', rn): + rn = rn[5:] + roles[rn] = (_text(v) == 'true') + o = OrderedDict() + o['common'] = common + o['roles'] = roles + return o + + +# Командный интерфейс формы (): панели CommandBar + NavigationPanel. +def decompile_command_interface(): + ci_node = ROOT.find('lf:CommandInterface', NS) + if ci_node is None: + return None + ci = OrderedDict() + for panel in (('CommandBar', 'commandBar'), ('NavigationPanel', 'navigationPanel')): + pn = ci_node.find('lf:%s' % panel[0], NS) + if pn is None: + continue + items = [] + for it in pn.findall('lf:Item', NS): + o = OrderedDict() + cmd = get_child(it, 'Command') + o['command'] = _ps_str(cmd) + ty = get_child(it, 'Type') + if ty and ty != 'Auto': + o['type'] = ty + at = get_child(it, 'Attribute') + if at: + o['attribute'] = at + cg = get_child(it, 'CommandGroup') + if cg: + o['group'] = cg + idx = get_child(it, 'Index') + if idx is not None: + o['index'] = int(idx) + dv = get_child(it, 'DefaultVisible') + if dv is not None: + o['defaultVisible'] = (dv == 'true') + vis = decompile_xr_flag(it, 'Visible') + if vis is not None: + o['visible'] = vis + # Голый элемент (только command) → строка-shorthand; иначе объект + if len(o) == 1: + items.append(_ps_str(cmd)) + else: + items.append(o) + if len(items) > 0: + ci[panel[1]] = items + if len(ci) > 0: + return ci + return None + + +# FunctionalOption.X…> → массив строк (префикс снят; GUID — как есть). +def decompile_functional_options(node): + fo_node = node.find('lf:FunctionalOptions', NS) + if fo_node is None: + return None + opts = [] + for it in fo_node.findall('lf:Item', NS): + t = re.sub(r'^FunctionalOption\.', '', (_text(it) or '').strip()) + opts.append(t) + if len(opts) > 0: + return opts + return None + + +# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions. +def decompile_attr_column(c): + co = OrderedDict() + co['name'] = _attr(c, 'name') + cty = decompile_type(c.find('lf:Type', NS)) + if cty: + co['type'] = cty + ct_node = c.find('lf:Title', NS) + if ct_node is not None: + t = get_lang_text_ws(ct_node) + if t is not None: + co['title'] = t + cfc = get_child(c, 'FillCheck') + if cfc: + co['fillCheck'] = cfc + cfo = decompile_functional_options(c) + if _ps_truthy(cfo): + co['functionalOptions'] = cfo + cv = decompile_xr_flag(c, 'View') + if cv is not None: + co['view'] = cv + ce = decompile_xr_flag(c, 'Edit') + if ce is not None: + co['edit'] = ce + return co + + +# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/…). Дефолт loadTransparent=false. +def get_picture_ref(node, pic_tag): + ref = node.find('lf:%s/xr:Ref' % pic_tag, NS) + abs_ = node.find('lf:%s/xr:Abs' % pic_tag, NS) + if ref is None and abs_ is None: + return None + src = _text(ref) if ref is not None else 'abs:%s' % _text(abs_) + lt = node.find('lf:%s/xr:LoadTransparent' % pic_tag, NS) + lt_true = (lt is not None and _text(lt) == 'true') + tpx = node.find('lf:%s/xr:TransparentPixel' % pic_tag, NS) + if not lt_true and tpx is None: + return src + o = OrderedDict([('src', src)]) + if lt_true: + o['loadTransparent'] = True + if tpx is not None: + o['transparentPixel'] = OrderedDict([('x', int(_attr(tpx, 'x'))), ('y', int(_attr(tpx, 'y')))]) + return o + + +# кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция). +def set_command_picture(obj, node): + ref = node.find('lf:Picture/xr:Ref', NS) + abs_ = node.find('lf:Picture/xr:Abs', NS) + if ref is None and abs_ is None: + return + src = _text(ref) if ref is not None else 'abs:%s' % _text(abs_) + lt = node.find('lf:Picture/xr:LoadTransparent', NS) + lt_false = (lt is not None and _text(lt) == 'false') + tpx = node.find('lf:Picture/xr:TransparentPixel', NS) + if tpx is not None: + o = OrderedDict([('src', src)]) + if lt_false: + o['loadTransparent'] = False + o['transparentPixel'] = OrderedDict([('x', int(_attr(tpx, 'x'))), ('y', int(_attr(tpx, 'y')))]) + obj['picture'] = o + else: + obj['picture'] = src + if lt_false: + obj['loadTransparent'] = False + + +# Шрифт → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты. +def build_font_value(f): + present = [] + for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + if a in f.attrib: + present.append(a) + if len(present) == 2 and ('ref' in present) and _attr(f, 'kind') == 'StyleItem': + return _attr(f, 'ref') + o = OrderedDict() + for k in present: + v = _attr(f, k) + if k in ('height', 'scale') and re.match(r'^-?\d+$', v): + o[k] = int(v) + elif k in ('bold', 'italic', 'underline', 'strikeout'): + o[k] = (v == 'true') + else: + o[k] = v + return o + + +# Граница → строка-ref (из стиля) или объект {width, style}. +def build_border_value(b): + if 'ref' in b.attrib: + return _attr(b, 'ref') + o = OrderedDict() + if 'width' in b.attrib: + w = _attr(b, 'width') + o['width'] = int(w) if re.match(r'^-?\d+$', w) else w + st = b.find('v8ui:style', NS) + if st is not None: + o['style'] = _text(st) + return o + + +# Порядок ключей цвета = .NET Hashtable enumeration (захвачено из PS 5.1), НЕ порядок литерала. +COLOR_MAP = [ + ('BackColor', 'backColor'), + ('TitleBackColor', 'titleBackColor'), + ('TitleTextColor', 'titleTextColor'), + ('TextColor', 'textColor'), + ('FooterTextColor', 'footerTextColor'), + ('FooterBackColor', 'footerBackColor'), + ('BorderColor', 'borderColor'), +] + + +# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка. +def add_appearance(obj, node): + for tag, key in COLOR_MAP: + c = node.find('lf:%s' % tag, NS) + if c is not None: + obj[key] = _text(c) + for pair in (('Font', 'font'), ('TitleFont', 'titleFont'), ('FooterFont', 'footerFont')): + f = node.find('lf:%s' % pair[0], NS) + if f is not None: + obj[pair[1]] = build_font_value(f) + b = node.find('lf:Border', NS) + if b is not None: + obj['border'] = build_border_value(b) + + +def add_common_props(obj, node, el_name): + add_appearance(obj, node) + if get_child(node, 'Visible') == 'false': + obj['hidden'] = True + if get_child(node, 'Enabled') == 'false': + obj['disabled'] = True + if get_child(node, 'ReadOnly') == 'true': + obj['readOnly'] = True + uv = decompile_xr_flag(node, 'UserVisible') + if uv is not None: + obj['userVisible'] = uv + title_node = node.find('lf:Title', NS) + if title_node is not None: + t = get_lang_text_ws(title_node) + if t is not None: + obj['title'] = t + tt_node = node.find('lf:ToolTip', NS) + if tt_node is not None: + tt = get_lang_text_ws(tt_node) + if tt is not None: + obj['tooltip'] = tt + ttr = get_child(node, 'ToolTipRepresentation') + if ttr: + obj['tooltipRepresentation'] = ttr + hp = get_picture_ref(node, 'HeaderPicture') + if hp is not None: + obj['headerPicture'] = hp + fp = get_picture_ref(node, 'FooterPicture') + if fp is not None: + obj['footerPicture'] = fp + ev = get_events(node, el_name) + if ev: + obj['events'] = ev + # CommandSet — список отключённых команд редактора (только ). + cs_node = node.find('lf:CommandSet', NS) + if cs_node is not None: + exc = [] + for ec in cs_node.findall('lf:ExcludedCommand', NS): + exc.append(_text(ec)) + if len(exc) > 0: + obj['excludedCommands'] = exc + + +# --- 3. Type decompile (inverse of Emit-Type) --- +def decompile_type(type_node): + if type_node is None: + return None + parts = [] + for vt in type_node.findall('v8:Type', NS): + raw = (_text(vt) or '').strip() + short = raw + # break-эквивалент: ветви взаимоисключающи (общий v8|v8ui не перетирает специфичные). + if re.match(r'^xs:string$', raw): + ln = type_node.find('v8:StringQualifiers/v8:Length', NS) + al = type_node.find('v8:StringQualifiers/v8:AllowedLength', NS) + fixed = (al is not None and _text(al) == 'Fixed') # Variable = дефолт; Fixed — явно + if ln is not None and int(_text(ln)) > 0: + short = ('string(%s,fixed)' % _text(ln)) if fixed else ('string(%s)' % _text(ln)) + else: + short = 'string' # Length=0 → всегда Variable (корпус) + elif re.match(r'^xs:decimal$', raw): + d = type_node.find('v8:NumberQualifiers/v8:Digits', NS) + f = type_node.find('v8:NumberQualifiers/v8:FractionDigits', NS) + sgn = type_node.find('v8:NumberQualifiers/v8:AllowedSign', NS) + dd = _text(d) if d is not None else '0' + ff = _text(f) if f is not None else '0' + if sgn is not None and _text(sgn) == 'Nonnegative': + short = 'decimal(%s,%s,nonneg)' % (dd, ff) + else: + short = 'decimal(%s,%s)' % (dd, ff) + elif re.match(r'^xs:boolean$', raw): + short = 'boolean' + elif re.match(r'^xs:dateTime$', raw): + df = type_node.find('v8:DateQualifiers/v8:DateFractions', NS) + dfv = _text(df) if df is not None else 'DateTime' + if dfv == 'Date': + short = 'date' + elif dfv == 'Time': + short = 'time' + else: + short = 'dateTime' + else: + m_cfg = re.match(r'^cfg:(.+)$', raw) + if m_cfg: + short = m_cfg.group(1) + elif re.match(r'^(v8|v8ui):', raw): + # Платформенный тип: friendly-шорткат если есть, иначе verbatim (не теряем v8:UUID и т.п.). + rev = { + 'v8:ValueTable': 'ValueTable', 'v8:ValueTree': 'ValueTree', 'v8:ValueListType': 'ValueList', + 'v8:TypeDescription': 'TypeDescription', 'v8:Universal': 'Universal', + 'v8:FixedArray': 'FixedArray', 'v8:FixedStructure': 'FixedStructure', + 'v8ui:FormattedString': 'FormattedString', 'v8ui:Picture': 'Picture', 'v8ui:Color': 'Color', 'v8ui:Font': 'Font', + } + short = rev.get(raw, raw) + else: + short = raw + parts.append(short) + # TypeSet (набор типов): префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена. + for ts in type_node.findall('v8:TypeSet', NS): + raw = (_text(ts) or '').strip() + short = re.sub(r'^(v8ui|v8|cfg):', '', raw) + parts.append(short) + # TypeId — тип по глобальному стабильному GUID → маркер 'typeid:GUID' (компилятор разворачивает). + for ti in type_node.findall('v8:TypeId', NS): + parts.append('typeid:' + (_text(ti) or '').strip()) + if len(parts) == 0: + return None + if len(parts) == 1: + return parts[0] + return ' | '.join(parts) + + +# Ограничения использования (useRestriction/attributeUseRestriction) → объект {field?,condition?,group?,order?}. +def build_restrict_obj(node): + r = OrderedDict() + for k in ('field', 'condition', 'group', 'order'): + if get_child(node, k) == 'true': + r[k] = True + return r + + +# Вычисляемое поле DataSet динамического списка () → объектная модель. +def build_calc_field(cf_node): + o = OrderedDict() + o['dataPath'] = get_child(cf_node, 'dataPath') + o['expression'] = get_child(cf_node, 'expression') + tn = cf_node.find('dcssch:title', NS) + if tn is not None: + t = get_lang_text(tn) + if t is not None: + o['title'] = t + vt = cf_node.find('dcssch:valueType', NS) + if vt is not None: + v = decompile_type(vt) + if v: + o['valueType'] = v + ur = cf_node.find('dcssch:useRestriction', NS) + if ur is not None: + r = OrderedDict() + for k in ('field', 'condition', 'group', 'order'): + if get_child(ur, k) == 'true': + r[k] = True + if len(r) > 0: + o['useRestriction'] = r + pe = get_child(cf_node, 'presentationExpression') + if pe is not None and pe != '': + o['presentationExpression'] = pe + oe_nodes = cf_node.findall('dcssch:orderExpression', NS) + if len(oe_nodes) > 0: + oes = [] + for oen in oe_nodes: + eo = OrderedDict() + expr_n = oen.find('{*}expression') + ot_n = oen.find('{*}orderType') + ao_n = oen.find('{*}autoOrder') + eo['expression'] = _text(expr_n) if expr_n is not None else '' + if ot_n is not None and _text(ot_n) != 'Asc': + eo['orderType'] = _text(ot_n) + if ao_n is not None and _text(ao_n) == 'true': + eo['autoOrder'] = True + oes.append(eo) + o['orderExpression'] = oes + return o + + +# Параметры выбора входа дин-списка () → массив. Зеркало Emit-DLInputParameters. +def build_dl_input_parameters(ip_node): + items = [] + for it in ip_node.findall('dcscor:item', NS): + io = OrderedDict() + io['parameter'] = get_child(it, 'parameter') + use_n = it.find('dcscor:use', NS) + if use_n is not None and _text(use_n) == 'false': + io['use'] = False + val_n = it.find('dcscor:value', NS) + if val_n is not None: + vt = _attr(val_n, 'type', NS_XSI) + if re.search(r'ChoiceParameters$', vt): + cps = [] + for cpi in val_n.findall('dcscor:item', NS): + cpo = OrderedDict() + cpo['name'] = get_child(cpi, 'choiceParameter') + vals = [] + for cv in cpi.findall('dcscor:value', NS): + vals.append(convert_typed_value(_text(cv), _attr(cv, 'type', NS_XSI))) + cpo['values'] = vals + cps.append(cpo) + io['choiceParameters'] = cps + elif re.search(r'ChoiceParameterLinks$', vt): + cpls = [] + for cpi in val_n.findall('dcscor:item', NS): + cpo = OrderedDict() + cpo['name'] = get_child(cpi, 'choiceParameter') + cpo['value'] = get_child(cpi, 'value') + md = get_child(cpi, 'mode') + if md and md != 'Auto': + cpo['mode'] = md + cpls.append(cpo) + io['choiceParameterLinks'] = cpls + elif re.search(r'TypeLink$', vt): + # Связь по типу: field + linkItem — структурное значение, НЕ склеивать InnerText в строку. + tlo = OrderedDict() + tlf = get_child(val_n, 'field') + if tlf is not None: + tlo['field'] = tlf + tli = get_child(val_n, 'linkItem') + if tli is not None: + tlo['linkItem'] = int(tli) if re.match(r'^-?\d+$', tli) else tli + io['typeLink'] = tlo + else: + if _attr(val_n, 'nil', NS_XSI) != 'true': + io['value'] = convert_typed_value(_text(val_n), vt) + items.append(io) + return items + + +# dcsset:dataParameters → массив (shorthand "Имя @off" / объект для типизированного значения). +def build_form_data_parameters(dp_node): + entries = [] + for it in dp_node.findall('dcscor:item', NS): + pn = get_text(it, 'dcscor:parameter') + use = get_text(it, 'dcscor:use') + val_nodes = it.findall('dcscor:value', NS) + val_node = val_nodes[0] if len(val_nodes) >= 1 else None + usid_n = it.find('dcsset:userSettingID', NS) + vm_n = it.find('dcsset:viewMode', NS) + usp_n = it.find('dcsset:userSettingPresentation', NS) + if val_node is not None or usid_n is not None or vm_n is not None or usp_n is not None: + obj = OrderedDict([('parameter', pn)]) + if len(val_nodes) > 1: + # Список значений параметра (valueListAllowed) — все массивом + obj['value'] = [_text(v) for v in val_nodes] + vt0 = _attr(val_nodes[0], 'type', NS_XSI) + if vt0: + obj['valueType'] = vt0 + elif val_node is not None: + if _attr(val_node, 'nil', NS_XSI) == 'true': + obj['nilValue'] = True + else: + v_type = _attr(val_node, 'type', NS_XSI) + v_val = _text(val_node) + if re.search(r'decimal$', v_type) and re.match(r'^-?\d+$', v_val): + obj['value'] = int(v_val) + elif re.search(r'boolean$', v_type): + obj['value'] = (v_val == 'true') + else: + obj['value'] = v_val + if v_type: + obj['valueType'] = v_type + if use == 'false': + obj['use'] = False + if usid_n is not None: + obj['userSettingID'] = 'auto' + if vm_n is not None: + obj['viewMode'] = _text(vm_n) + if usp_n is not None: + usp = get_pres_text(usp_n) + if usp is not None: + obj['userSettingPresentation'] = usp + entries.append(obj) + else: + s = pn + if use == 'false': + s += ' @off' + entries.append(s) + return entries + + +def build_dl_parameter(p_node): + name = get_child(p_node, 'name') + o = OrderedDict() + o['name'] = name + # title — опускаем, если совпадает с авто-выводом из имени (ru-only) + title_node = p_node.find('dcssch:title', NS) + if title_node is not None: + t = get_lang_text(title_node) + if t is not None: + auto = title_from_name(name) + if not (isinstance(t, str) and _ps_ieq(t, auto)): + o['title'] = t + vt_node = p_node.find('dcssch:valueType', NS) + type_val = None + if vt_node is not None: + type_val = decompile_type(vt_node) + if type_val: + o['type'] = type_val + # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil (явный маркер value:null). + v_nodes = p_node.findall('dcssch:value', NS) + if len(v_nodes) > 1: + o['value'] = [convert_typed_value(_text(v), _attr(v, 'type', NS_XSI)) for v in v_nodes] + elif len(v_nodes) == 1: + v_node = v_nodes[0] + if _attr(v_node, 'nil', NS_XSI) != 'true': + o['value'] = convert_typed_value(_text(v_node), _attr(v_node, 'type', NS_XSI)) + elif get_child(p_node, 'valueListAllowed') == 'true': + o['value'] = None + if get_child(p_node, 'useRestriction') == 'false': + o['useRestriction'] = False + expr = get_child(p_node, 'expression') + if expr is not None and expr != '': + o['expression'] = expr + av_nodes = p_node.findall('dcssch:availableValue', NS) + if len(av_nodes) > 0: + avs = [] + for avn in av_nodes: + avo = OrderedDict() + avv = avn.find('dcssch:value', NS) + if avv is not None and _attr(avv, 'nil', NS_XSI) != 'true': + avo['value'] = convert_typed_value(_text(avv), _attr(avv, 'type', NS_XSI)) + else: + avo['value'] = None + avp = avn.find('dcssch:presentation', NS) + if avp is not None: + pres = get_lang_text(avp) + if pres is not None: + avo['presentation'] = pres + avs.append(avo) + o['availableValues'] = avs + if get_child(p_node, 'valueListAllowed') == 'true': + o['valueListAllowed'] = True + if get_child(p_node, 'availableAsField') == 'false': + o['availableAsField'] = False + ip_node = p_node.find('dcssch:inputParameters', NS) + if ip_node is not None: + ip = build_dl_input_parameters(ip_node) + if len(ip) > 0: + o['inputParameters'] = ip[0] if len(ip) == 1 else ip # PS unwrap @() одноэлементного → объект + if get_child(p_node, 'denyIncompleteValues') == 'true': + o['denyIncompleteValues'] = True + use = get_child(p_node, 'use') + if use is not None and use != '': + o['use'] = use + + # Компактизация: {name} → "name"; {name, type} → "name: type"; иначе объект. + keys = list(o.keys()) + if len(keys) == 1: + return name + if len(keys) == 2 and 'type' in o and isinstance(type_val, str): + return '%s: %s' % (name, type_val) + return o + + +# --- 4. Element dispatch --- +# Lookup-карта (диспетчер по тегу → DSL-ключ); порядок не влияет на вывод. +ELEMENT_KEY = { + 'UsualGroup': 'group', 'ColumnGroup': 'columnGroup', 'ButtonGroup': 'buttonGroup', 'InputField': 'input', 'CheckBoxField': 'check', + 'RadioButtonField': 'radio', 'LabelDecoration': 'label', 'LabelField': 'labelField', + 'PictureDecoration': 'picture', 'PictureField': 'picField', 'CalendarField': 'calendar', + 'Table': 'table', 'Pages': 'pages', 'Page': 'page', 'Button': 'button', 'CommandBar': 'cmdBar', 'Popup': 'popup', + 'SearchStringAddition': 'searchString', 'ViewStatusAddition': 'viewStatus', 'SearchControlAddition': 'searchControl', + 'SpreadSheetDocumentField': 'spreadsheet', 'HTMLDocumentField': 'html', 'TextDocumentField': 'textDoc', + 'FormattedDocumentField': 'formattedDoc', 'ProgressBarField': 'progressBar', 'TrackBarField': 'trackBar', + 'ChartField': 'chart', 'GraphicalSchemaField': 'graphicalSchema', 'PlannerField': 'planner', + 'PeriodField': 'periodField', 'DendrogramField': 'dendrogram', 'GanttChartField': 'ganttChart', +} + +# Простые скаляры элемента (pass-through, зеркало $script:genericScalars). ПОРЯДОК значим (ordered). +GENERIC_SCALARS = [ + ('VerticalAlign', 'verticalAlign', 'value'), + ('ThroughAlign', 'throughAlign', 'value'), + ('EnableContentChange', 'enableContentChange', 'bool'), + ('PictureSize', 'pictureSize', 'value'), + ('TitleHeight', 'titleHeight', 'value'), + ('ChildItemsWidth', 'childItemsWidth', 'value'), + ('ShowLeftMargin', 'showLeftMargin', 'bool'), + ('CellHyperlink', 'cellHyperlink', 'bool'), + ('ViewMode', 'viewMode', 'value'), + ('VerticalScrollBar', 'verticalScrollBar', 'value'), + ('RowInputMode', 'rowInputMode', 'value'), + ('Mask', 'mask', 'value'), + ('CreateButton', 'createButton', 'bool'), + ('FixingInTable', 'fixingInTable', 'value'), + ('VerticalSpacing', 'verticalSpacing', 'value'), + ('HorizontalScrollBar', 'horizontalScrollBar', 'value'), + ('ViewScalingMode', 'viewScalingMode', 'value'), + ('Output', 'output', 'value'), + ('SelectionShowMode', 'selectionShowMode', 'value'), + ('PointerType', 'pointerType', 'value'), + ('DrawingSelectionShowMode', 'drawingSelectionShowMode', 'value'), + ('WarningOnEditRepresentation', 'warningOnEditRepresentation', 'value'), + ('MarkingAppearance', 'markingAppearance', 'value'), + ('Protection', 'protection', 'bool'), + ('Edit', 'edit', 'bool'), + ('ShowGrid', 'showGrid', 'bool'), + ('ShowGroups', 'showGroups', 'bool'), + ('ShowHeaders', 'showHeaders', 'bool'), + ('ShowRowAndColumnNames', 'showRowAndColumnNames', 'bool'), + ('ShowCellNames', 'showCellNames', 'bool'), + ('ShowPercent', 'showPercent', 'bool'), + ('HorizontalSpacing', 'horizontalSpacing', 'value'), + ('RepresentationInContextMenu', 'representationInContextMenu', 'value'), + ('SettingsNamedItemDetailedRepresentation', 'settingsNamedItemDetailedRepresentation', 'bool'), + ('ItemHeight', 'itemHeight', 'value'), + ('DropListWidth', 'dropListWidth', 'value'), + ('TitleDataPath', 'titleDataPath', 'value'), + ('ExtendedEdit', 'extendedEdit', 'bool'), + ('MaxRowsCount', 'maxRowsCount', 'value'), + ('AutoMaxRowsCount', 'autoMaxRowsCount', 'bool'), + ('HeightControlVariant', 'heightControlVariant', 'value'), + ('EditTextUpdate', 'editTextUpdate', 'value'), + ('ControlRepresentation', 'controlRepresentation', 'value'), + ('ShapeRepresentation', 'shapeRepresentation', 'value'), + ('AutoAddIncomplete', 'autoAddIncomplete', 'bool'), + ('MarkNegatives', 'markNegatives', 'bool'), + ('InitialListView', 'initialListView', 'value'), + ('ChoiceListHeight', 'choiceListHeight', 'value'), + ('ThreeState', 'threeState', 'bool'), + ('ScrollOnCompress', 'scrollOnCompress', 'bool'), + ('Shortcut', 'shortcut', 'value'), + ('IncompleteChoiceMode', 'incompleteChoiceMode', 'value'), + ('EqualColumnsWidth', 'equalColumnsWidth', 'bool'), + ('ChildrenAlign', 'childrenAlign', 'value'), + ('ImageScale', 'imageScale', 'value'), + ('Zoomable', 'zoomable', 'bool'), + ('Shape', 'shape', 'value'), + ('PictureLocation', 'pictureLocation', 'value'), + ('EqualItemsWidth', 'equalItemsWidth', 'bool'), + ('ItemTitleHeight', 'itemTitleHeight', 'value'), + ('SpecialTextInputMode', 'specialTextInputMode', 'value'), + ('ItemWidth', 'itemWidth', 'value'), + ('ShowCheckBoxesInDropList', 'showCheckBoxesInDropList', 'bool'), + ('MultipleValueDataPath', 'multipleValueDataPath', 'value'), + ('MultipleValuePresentDataPath', 'multipleValuePresentDataPath', 'value'), + ('AutoShowOpenButtonMode', 'autoShowOpenButtonMode', 'value'), + ('AutoShowClearButtonMode', 'autoShowClearButtonMode', 'value'), + ('MultipleValuesTextColor', 'multipleValuesTextColor', 'value'), + ('MultipleValuesBackColor', 'multipleValuesBackColor', 'value'), + ('MultipleValuePictureShape', 'multipleValuePictureShape', 'value'), + ('MultipleValuePictureDataPath', 'multipleValuePictureDataPath', 'value'), + ('AutoCorrectionOnTextInput', 'autoCorrectionOnTextInput', 'value'), + ('SpellCheckingOnTextInput', 'spellCheckingOnTextInput', 'value'), + ('CommandUniqueness', 'commandUniqueness', 'bool'), + ('AllowInputEmptyMultipleValues', 'allowInputEmptyMultipleValues', 'bool'), + ('BehaviorOnHorizontalCompression', 'behaviorOnHorizontalCompression', 'value'), +] + + +# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает. +def add_generic_scalars(obj, node): + for tag, key, kind in GENERIC_SCALARS: + if key in obj: + continue + v = get_child(node, tag) + if v is None: + continue + if kind == 'bool': + obj[key] = (v == 'true') + else: + obj[key] = v + + +def decompile_children(parent_node, child_container='ChildItems'): + container = parent_node.find('lf:%s' % child_container, NS) + if container is None: + return None + lst = [] + for child in list(container): + if _local_name(child.tag) in COMPANION_TAGS: + continue + el = decompile_element(child) + if el is not None: + lst.append(el) + if len(lst) == 0: + return None + return lst + + +# Инверсия Emit-CompanionPanel: companion-панель (ContextMenu/AutoCommandBar) с контентом → объект либо None. +def decompile_companion_panel(node, tag, is_dyn_list_table=False): + p = node.find('lf:%s' % tag, NS) + if p is None: + return None + autofill_raw = get_child(p, 'Autofill') + halign = get_child(p, 'HorizontalAlign') + kids = decompile_children(p) + has_kids = kids is not None and len(kids) > 0 + if is_dyn_list_table and tag == 'AutoCommandBar' and not has_kids and not halign: + if autofill_raw == 'false': + return None # = дефолт эвристики → молчим + return OrderedDict([('autofill', True)]) # голая панель → отклонение + if not has_kids and autofill_raw is None and not halign: + return None + o = OrderedDict() + if halign: + o['horizontalAlign'] = halign + if autofill_raw == 'false': + o['autofill'] = False + elif autofill_raw == 'true': + o['autofill'] = True + if has_kids: + o['children'] = kids + return o + + +# Инверсия Emit-ChoiceList: … → [ { value, presentation? } ] либо None. +def decompile_choice_list(node): + cl = node.find('lf:ChoiceList', NS) + if cl is None: + return None + items = [] + for it in cl.findall('xr:Item', NS): + val_node = it.find('xr:Value/lf:Value', NS) + pres_node = it.find('xr:Value/lf:Presentation', NS) + ci = OrderedDict() + if val_node is not None: + if _attr(val_node, 'nil', NS_XSI) == 'true': + # nil-значение — компилятор эмитит . + ci['valueType'] = 'nil' + else: + xsi_type = _attr(val_node, 'type', NS_XSI) + ci['value'] = convert_typed_value(_text(val_node), xsi_type) + # Не-примитивный тип / raw-ссылка по GUID → сохраняем valueType. + if xsi_type and not re.match(r'^xs:(string|decimal|boolean|dateTime)$', xsi_type) and \ + (xsi_type != 'xr:DesignTimeRef' or re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-', _text(val_node) or '')): + ci['valueType'] = xsi_type + # Presentation: непустой → текст; пустой → "" суппресс-маркер. + if pres_node is not None: + p = get_lang_text_ws(pres_node) + if p is not None and p != '': + ci['presentation'] = p + else: + ci['presentation'] = '' + items.append(ci) + if len(items) > 0: + return items + return None + + +# Значение Параметра выбора (): скаляр или v8:FixedArray → массив скаляров. +def convert_choice_param_value(val_node): + vt = _attr(val_node, 'type', NS_XSI) + if re.search(r'FixedArray$', vt): + arr = [] + for it in val_node.findall('v8:Value', NS): + inner = it.find('lf:Value', NS) + if inner is not None: + arr.append(convert_typed_value(_text(inner), _attr(inner, 'type', NS_XSI))) + return arr + return convert_typed_value(_text(val_node), vt) + + +# Инверсия Emit-ChoiceParameters: → [{name, value}]. +def decompile_choice_parameters(node): + cpn = node.find('lf:ChoiceParameters', NS) + if cpn is None: + return None + items = [] + for it in cpn.findall('app:item', NS): + o = OrderedDict() + o['name'] = _attr(it, 'name') + val_node = it.find('app:value/lf:Value', NS) + if val_node is not None: + o['value'] = convert_choice_param_value(val_node) + items.append(o) + if len(items) > 0: + return items + return None + + +# Инверсия Emit-ChoiceParameterLinks → [{name, dataPath, valueChange?}]. +def decompile_choice_parameter_links(node): + cln = node.find('lf:ChoiceParameterLinks', NS) + if cln is None: + return None + items = [] + for lk in cln.findall('xr:Link', NS): + o = OrderedDict() + o['name'] = get_text(lk, 'xr:Name') + o['dataPath'] = get_text(lk, 'xr:DataPath') + vc = get_text(lk, 'xr:ValueChange') + if vc and vc != 'Clear': + o['valueChange'] = vc + items.append(o) + if len(items) > 0: + return items + return None + + +# Инверсия Emit-TypeLink: → {dataPath, linkItem}. +def decompile_type_link(node): + tn = node.find('lf:TypeLink', NS) + if tn is None: + return None + o = OrderedDict() + o['dataPath'] = get_text(tn, 'xr:DataPath') + li = get_text(tn, 'xr:LinkItem') + if li is not None and li != '': + o['linkItem'] = int(li) + return o + + +# Захват / (LocalStringType) → format/editFormat. +def add_format_props(obj, node): + fmt = node.find('lf:Format', NS) + if fmt is not None: + t = get_lang_text(fmt) + if t is not None and t != '': + obj['format'] = t + efmt = node.find('lf:EditFormat', NS) + if efmt is not None: + t = get_lang_text(efmt) + if t is not None and t != '': + obj['editFormat'] = t + + +# Ядро дополнения: source + Add-CommonProps + horizontalLocation. Layout добавляется отдельно. +def add_addition_core(obj, node, el_name): + src = node.find('lf:AdditionSource/lf:Item', NS) + if src is not None: + obj['source'] = _text(src) + add_common_props(obj, node, el_name) + hl = get_child(node, 'HorizontalLocation') + if hl: + obj['horizontalLocation'] = hl.lower() + + +# Стандартные дополнения уровня таблицы → карта { тип: {отклонения} }. +def decompile_table_additions(table_node, table_name): + tag_to_key = {'SearchStringAddition': 'searchString', 'ViewStatusAddition': 'viewStatus', 'SearchControlAddition': 'searchControl'} + m = OrderedDict() + for child in list(table_node): + ln = _local_name(child.tag) + if ln not in tag_to_key: + continue + key = tag_to_key[ln] + nm = _attr(child, 'name') + o = OrderedDict() + o[key] = nm + add_addition_core(o, child, nm) + add_layout(o, child) + del o[key] # имя авто + if 'source' in o and o['source'] == table_name: + del o['source'] # source=таблица дефолт + if len(o) > 0: + m[key] = o + if len(m) > 0: + return m + return None + + +# Спец-поля «документ/датчик» — общий скелет поля. +def decompile_simple_field(obj, node, name, key): + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + tl = get_child(node, 'TitleLocation') + if tl: + obj['titleLocation'] = tl.lower() + em = get_child(node, 'EditMode') + if em: + obj['editMode'] = em + + +# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type. +def add_gauge_scalars(obj, node, tags): + for p in tags: + v = get_child(node, p) + if v is None: + continue + key = p[0:1].lower() + p[1:] + if re.match(r'^-?\d+$', v): + obj[key] = int(v) + else: + obj[key] = v + + +def decompile_element(node): + tag = _local_name(node.tag) + if tag not in ELEMENT_KEY: + fail_ring3("элемент <%s>" % tag, "ChildItems/%s" % tag) + key = ELEMENT_KEY[tag] + name = _attr(node, 'name') + obj = OrderedDict() + + if tag == 'UsualGroup': + g = get_child(node, 'Group') + gmap = {'Horizontal': 'horizontal', 'Vertical': 'vertical', 'AlwaysHorizontal': 'alwaysHorizontal', 'AlwaysVertical': 'alwaysVertical', 'HorizontalIfPossible': 'horizontalIfPossible'} + if g and g in gmap: + obj[key] = gmap[g] + else: + obj[key] = '' + behavior = get_child(node, 'Behavior') + if behavior: + bmap = {'Usual': 'usual', 'Collapsible': 'collapsible', 'PopUp': 'popup'} + obj['behavior'] = bmap[behavior] if behavior in bmap else behavior + obj['name'] = name + add_common_props(obj, node, name) + rep = get_child(node, 'Representation') + if rep: + repmap = {'None': 'none', 'NormalSeparation': 'normal', 'WeakSeparation': 'weak', 'StrongSeparation': 'strong'} + obj['representation'] = repmap[rep] if rep in repmap else rep + st = get_child(node, 'ShowTitle') + if st is not None: + obj['showTitle'] = (st == 'true') + cru = get_child(node, 'CurrentRowUse') + if cru: + obj['currentRowUse'] = cru + crt = node.find('lf:CollapsedRepresentationTitle', NS) + if crt is not None: + ct = get_lang_text(crt) + if ct is not None and ct != '': + obj['collapsedTitle'] = ct + if get_child(node, 'United') == 'false': + obj['united'] = False + if get_child(node, 'Collapsed') == 'true': + obj['collapsed'] = True + add_format_props(obj, node) + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'ColumnGroup': + g = get_child(node, 'Group') + gmap = {'Horizontal': 'horizontal', 'Vertical': 'vertical', 'InCell': 'inCell'} + if g and g in gmap: + obj[key] = gmap[g] + else: + obj[key] = '' + obj['name'] = name + add_common_props(obj, node, name) + st = get_child(node, 'ShowTitle') + if st is not None: + obj['showTitle'] = (st == 'true') + sih = get_child(node, 'ShowInHeader') + if sih is not None: + obj['showInHeader'] = to_bool(sih) + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'InputField': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + ml_in = get_child(node, 'MultiLine') + if ml_in is not None: + obj['multiLine'] = (ml_in == 'true') + pm_in = get_child(node, 'PasswordMode') + if pm_in is not None: + obj['passwordMode'] = (pm_in == 'true') + mi = get_child(node, 'AutoMarkIncomplete') + if mi is not None: + obj['markIncomplete'] = (mi == 'true') + em = get_child(node, 'EditMode') + if em: + obj['editMode'] = em + tl = get_child(node, 'TitleLocation') + if tl: + obj['titleLocation'] = tl.lower() + ih = node.find('lf:InputHint', NS) + if ih is not None: + t = get_lang_text_ws(ih) + if t: + obj['inputHint'] = t + woe = node.find('lf:WarningOnEdit', NS) + if woe is not None: + t = get_lang_text_ws(woe) + if t is not None: + obj['warningOnEdit'] = t + ftxt = node.find('lf:FooterText', NS) + if ftxt is not None: + t = get_lang_text_ws(ftxt) + if t is not None: + obj['footerText'] = t + for p in ('ChoiceButton', 'ClearButton', 'SpinButton', 'DropListButton', 'ChoiceListButton'): + v = get_child(node, p) + if v is not None: + obj[p[0:1].lower() + p[1:]] = to_bool(v) + for p in ('Wrap', 'OpenButton', 'ListChoiceMode', 'ExtendedEditMultipleValues', 'ChooseType', 'QuickChoice', 'AutoChoiceIncomplete'): + v = get_child(node, p) + if v is not None: + obj[p[0:1].lower() + p[1:]] = to_bool(v) + for p in ('ChoiceForm', 'ChoiceHistoryOnInput', 'ChoiceFoldersAndItems', 'FooterDataPath'): + v = get_child(node, p) + if v is not None: + obj[p[0:1].lower() + p[1:]] = v + for p in ('MinValue', 'MaxValue'): + mn = node.find('lf:%s' % p, NS) + if mn is not None: + xt = _attr(mn, 'type', NS_XSI) + txt = _text(mn) + k = p[0:1].lower() + p[1:] + if re.search(r'decimal|int', xt): + if re.match(r'^-?\d+$', txt): + obj[k] = int(txt) + elif re.match(r'^-?\d+\.\d+$', txt): + obj[k] = Decimal(txt) + else: + obj[k] = txt + else: + obj[k] = txt + tde = get_child(node, 'TypeDomainEnabled') + if tde is not None: + obj['typeDomainEnabled'] = to_bool(tde) + at_node = node.find('lf:AvailableTypes', NS) + if at_node is not None: + at = decompile_type(at_node) + if at: + obj['availableTypes'] = at + cbr = get_child(node, 'ChoiceButtonRepresentation') + if cbr: + obj['choiceButtonRepresentation'] = cbr + cbp = get_picture_ref(node, 'ChoiceButtonPicture') + if cbp is not None: + obj['choiceButtonPicture'] = cbp + if get_child(node, 'TextEdit') == 'false': + obj['textEdit'] = False + cl = decompile_choice_list(node) + if cl: + obj['choiceList'] = cl + add_format_props(obj, node) + cp = decompile_choice_parameters(node) + if cp: + obj['choiceParameters'] = cp + cpl = decompile_choice_parameter_links(node) + if cpl: + obj['choiceParameterLinks'] = cpl + tlk = decompile_type_link(node) + if tlk: + obj['typeLink'] = tlk + elif tag == 'CheckBoxField': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + em = get_child(node, 'EditMode') + if em: + obj['editMode'] = em + cbt = get_child(node, 'CheckBoxType') + if cbt is None: + obj['checkBoxType'] = '' + elif cbt != 'Auto': + obj['checkBoxType'] = cbt[0:1].lower() + cbt[1:] + add_title_location(obj, node, 'Right') + woe = node.find('lf:WarningOnEdit', NS) + if woe is not None: + t = get_lang_text_ws(woe) + if t is not None: + obj['warningOnEdit'] = t + fdp = get_child(node, 'FooterDataPath') + if fdp: + obj['footerDataPath'] = fdp + ftxt = node.find('lf:FooterText', NS) + if ftxt is not None: + t = get_lang_text_ws(ftxt) + if t is not None: + obj['footerText'] = t + add_format_props(obj, node) + elif tag == 'RadioButtonField': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + add_title_location(obj, node, 'None') + em = get_child(node, 'EditMode') + if em: + obj['editMode'] = em + rbt = get_child(node, 'RadioButtonType') + if rbt: + obj['radioButtonType'] = rbt + cc = get_child(node, 'ColumnsCount') + if cc: + obj['columnsCount'] = int(cc) + woe = node.find('lf:WarningOnEdit', NS) + if woe is not None: + t = get_lang_text_ws(woe) + if t is not None: + obj['warningOnEdit'] = t + cl = decompile_choice_list(node) + if cl: + obj['choiceList'] = cl + elif tag == 'LabelDecoration': + obj[key] = name + add_common_props(obj, node, name) + if get_child(node, 'Hyperlink') == 'true': + obj['hyperlink'] = True + ti_node = node.find('lf:Title', NS) + if ti_node is not None: + tv = get_ml_formatted_value(ti_node) + if tv is not None: + obj['title'] = tv + elif tag == 'LabelField': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + tl = get_child(node, 'TitleLocation') + if tl: + obj['titleLocation'] = tl.lower() + em = get_child(node, 'EditMode') + if em: + obj['editMode'] = em + if get_child(node, 'Hiperlink') == 'true': # тег (опечатка платформы) + obj['hyperlink'] = True + pm = get_child(node, 'PasswordMode') + if pm is not None: + obj['passwordMode'] = (pm == 'true') + woe = node.find('lf:WarningOnEdit', NS) + if woe is not None: + t = get_lang_text_ws(woe) + if t is not None: + obj['warningOnEdit'] = t + fdp = get_child(node, 'FooterDataPath') + if fdp: + obj['footerDataPath'] = fdp + ftxt = node.find('lf:FooterText', NS) + if ftxt is not None: + t = get_lang_text_ws(ftxt) + if t is not None: + obj['footerText'] = t + add_format_props(obj, node) + elif tag == 'PictureDecoration': + obj[key] = name + add_common_props(obj, node, name) + ti_node = node.find('lf:Title', NS) + if ti_node is not None: + tv = get_ml_formatted_value(ti_node) + if tv is not None: + obj['title'] = tv + npt = node.find('lf:NonselectedPictureText', NS) + if npt is not None: + t = get_lang_text_ws(npt) + if t is not None: + obj['nonselectedPictureText'] = t + ref = node.find('lf:Picture/xr:Ref', NS) + abs_ = node.find('lf:Picture/xr:Abs', NS) + if ref is not None: + obj['src'] = _text(ref) + elif abs_ is not None: + obj['src'] = 'abs:%s' % _text(abs_) + lt = node.find('lf:Picture/xr:LoadTransparent', NS) + if lt is not None and _text(lt) == 'true': + obj['loadTransparent'] = True + tpx = node.find('lf:Picture/xr:TransparentPixel', NS) + if tpx is not None: + obj['transparentPixel'] = OrderedDict([('x', int(_attr(tpx, 'x'))), ('y', int(_attr(tpx, 'y')))]) + if get_child(node, 'Hyperlink') == 'true': + obj['hyperlink'] = True + elif tag == 'PictureField': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + em = get_child(node, 'EditMode') + if em: + obj['editMode'] = em + tl = get_child(node, 'TitleLocation') + if tl: + obj['titleLocation'] = tl.lower() + if get_child(node, 'Hyperlink') == 'true': + obj['hyperlink'] = True + vp = get_picture_ref(node, 'ValuesPicture') + if vp is not None: + obj['valuesPicture'] = vp + npt = node.find('lf:NonselectedPictureText', NS) + if npt is not None: + t = get_lang_text_ws(npt) + if t is not None: + obj['nonselectedPictureText'] = t + fdp = get_child(node, 'FooterDataPath') + if fdp: + obj['footerDataPath'] = fdp + ftxt = node.find('lf:FooterText', NS) + if ftxt is not None: + t = get_lang_text_ws(ftxt) + if t is not None: + obj['footerText'] = t + elif tag == 'CalendarField': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + tl = get_child(node, 'TitleLocation') + if tl: + obj['titleLocation'] = tl.lower() + sm = get_child(node, 'SelectionMode') + if sm: + obj['selectionMode'] = sm + scd = get_child(node, 'ShowCurrentDate') + if scd is not None: + obj['showCurrentDate'] = (scd == 'true') + wim = get_child(node, 'WidthInMonths') + if wim is not None: + obj['widthInMonths'] = int(wim) + him = get_child(node, 'HeightInMonths') + if him is not None: + obj['heightInMonths'] = int(him) + smp = get_child(node, 'ShowMonthsPanel') + if smp is not None: + obj['showMonthsPanel'] = (smp == 'true') + elif tag == 'Table': + obj[key] = name + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + add_common_props(obj, node, name) + tl = get_child(node, 'TitleLocation') + if tl: + obj['titleLocation'] = tl.lower() + rep = get_child(node, 'Representation') + if rep: + obj['representation'] = rep + crs = get_child(node, 'ChangeRowSet') + if crs is not None: + obj['changeRowSet'] = (crs == 'true') + cro = get_child(node, 'ChangeRowOrder') + if cro is not None: + obj['changeRowOrder'] = (cro == 'true') + if get_child(node, 'AutoInsertNewRow') == 'true': + obj['autoInsertNewRow'] = True + if node.find('lf:RowFilter', NS) is not None: + obj['rowFilter'] = None + if get_child(node, 'Header') == 'false': + obj['header'] = False + if get_child(node, 'Footer') == 'true': + obj['footer'] = True + htr = get_child(node, 'HeightInTableRows') + if htr: + obj['heightInTableRows'] = int(htr) + hh = get_child(node, 'HeaderHeight') + if hh is not None: + obj['headerHeight'] = int(hh) + fh = get_child(node, 'FooterHeight') + if fh is not None: + obj['footerHeight'] = int(fh) + cru = get_child(node, 'CurrentRowUse') + if cru: + obj['currentRowUse'] = cru + rr = get_child(node, 'RefreshRequest') + if rr: + obj['refreshRequest'] = rr + # CommandBarLocation: дин-список-таблица авто-инжектит "None" → инвертируем суппресс-маркером. + cbl = get_child(node, 'CommandBarLocation') + if has_child(node, 'UpdateOnDataChange'): + if cbl is None: + obj['commandBarLocation'] = '' + elif cbl != 'None': + obj['commandBarLocation'] = cbl + elif cbl: + obj['commandBarLocation'] = cbl + ssl = get_child(node, 'SearchStringLocation') + if ssl: + obj['searchStringLocation'] = ssl + vsl = get_child(node, 'ViewStatusLocation') + if vsl: + obj['viewStatusLocation'] = vsl + scl = get_child(node, 'SearchControlLocation') + if scl: + obj['searchControlLocation'] = scl + if get_child(node, 'ChoiceMode') == 'true': + obj['choiceMode'] = True + selm = get_child(node, 'SelectionMode') + if selm: + obj['selectionMode'] = selm + rsm = get_child(node, 'RowSelectionMode') + if rsm: + obj['rowSelectionMode'] = rsm + if get_child(node, 'VerticalLines') == 'false': + obj['verticalLines'] = False + if get_child(node, 'HorizontalLines') == 'false': + obj['horizontalLines'] = False + if get_child(node, 'UseAlternationRowColor') == 'true': + obj['useAlternationRowColor'] = True + taf = get_child(node, 'Autofill') + if taf is not None: + obj['autofill'] = (taf == 'true') + if get_child(node, 'MultipleChoice') == 'true': + obj['multipleChoice'] = True + soin = get_child(node, 'SearchOnInput') + if soin: + obj['searchOnInput'] = soin + mi = get_child(node, 'AutoMarkIncomplete') + if mi is not None: + obj['markIncomplete'] = (mi == 'true') + itv = get_child(node, 'InitialTreeView') + if itv: + obj['initialTreeView'] = itv + rp = get_picture_ref(node, 'RowsPicture') + if rp is not None: + obj['rowsPicture'] = rp + rpdp = get_child(node, 'RowPictureDataPath') + if has_child(node, 'UpdateOnDataChange'): + if get_child(node, 'AutoRefresh') == 'true': + obj['autoRefresh'] = True + arp = get_child(node, 'AutoRefreshPeriod') + if arp and arp != '60': + obj['autoRefreshPeriod'] = int(arp) + cfi = get_child(node, 'ChoiceFoldersAndItems') + if cfi and cfi != 'Items': + obj['choiceFoldersAndItems'] = cfi + if get_child(node, 'RestoreCurrentRow') == 'true': + obj['restoreCurrentRow'] = True + if get_child(node, 'ShowRoot') == 'false': + obj['showRoot'] = False + if get_child(node, 'AllowRootChoice') == 'true': + obj['allowRootChoice'] = True + uodc = get_child(node, 'UpdateOnDataChange') + if uodc and uodc != 'Auto': + obj['updateOnDataChange'] = uodc + if get_child(node, 'AllowGettingCurrentRowURL') == 'false': + obj['allowGettingCurrentRowURL'] = False + if rpdp is None: + obj['rowPictureDataPath'] = '' + elif rpdp != ('%s.DefaultPicture' % _ps_str(obj.get('path'))): + obj['rowPictureDataPath'] = rpdp + usg = get_child(node, 'UserSettingsGroup') + if usg: + if re.match(r'^\d+:[0-9a-fA-F]{8}-', usg): + sys.stderr.write("form-decompile: UserSettingsGroup '%s' (%s) — ссылка по id, не воспроизводима, опущена\n" % (usg, name)) + else: + obj['userSettingsGroup'] = usg + elif rpdp: + obj['rowPictureDataPath'] = rpdp + cs_node = node.find('lf:CommandSet', NS) + if cs_node is not None: + exc = [] + for ec in cs_node.findall('lf:ExcludedCommand', NS): + exc.append(_text(ec)) + if len(exc) > 0: + obj['excludedCommands'] = exc + cols = decompile_children(node) + if cols: + obj['columns'] = cols + add_map = decompile_table_additions(node, name) + if add_map: + obj['additions'] = add_map + elif tag == 'Pages': + obj[key] = name + add_common_props(obj, node, name) + pr = get_child(node, 'PagesRepresentation') + if pr: + obj['pagesRepresentation'] = pr + cru = get_child(node, 'CurrentRowUse') + if cru: + obj['currentRowUse'] = cru + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'Page': + obj[key] = name + add_common_props(obj, node, name) + g = get_child(node, 'Group') + gmap = {'Horizontal': 'horizontal', 'Vertical': 'vertical', 'AlwaysHorizontal': 'alwaysHorizontal', 'AlwaysVertical': 'alwaysVertical', 'HorizontalIfPossible': 'horizontalIfPossible'} + if g and g in gmap: + obj['group'] = gmap[g] + pp = get_picture_ref(node, 'Picture') + if pp is not None: + obj['picture'] = pp + st = get_child(node, 'ShowTitle') + if st is not None: + obj['showTitle'] = (st == 'true') + add_format_props(obj, node) + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'Button': + obj[key] = name + cmd = get_child(node, 'CommandName') + if cmd: + m1 = re.match(r'^Form\.Command\.(.+)$', cmd) + m2 = re.match(r'^Form\.StandardCommand\.(.+)$', cmd) + m3 = re.match(r'^Form\.Item\.(.+)\.StandardCommand\.(.+)$', cmd) + if m1: + obj['command'] = m1.group(1) + elif m2: + obj['stdCommand'] = m2.group(1) + elif m3: + obj['stdCommand'] = '%s.%s' % (m3.group(1), m3.group(2)) + else: + obj['commandName'] = cmd + dp = get_child(node, 'DataPath') + if dp: + obj['path'] = dp + btn_param = node.find('lf:Parameter', NS) + if btn_param is not None: + pxt = _attr(btn_param, 'type', NS_XSI) + if re.search(r'TypeDescription$', pxt): + pt = decompile_type(btn_param) + if pt: + obj['parameter'] = OrderedDict([('type', pt)]) + elif _text(btn_param): + obj['parameter'] = _text(btn_param) + add_common_props(obj, node, name) + type_ = get_child(node, 'Type') + if type_: + tmap = {'CommandBarButton': 'commandBar', 'UsualButton': 'usual', 'Hyperlink': 'hyperlink', 'CommandBarHyperlink': 'hyperlink'} + obj['type'] = tmap[type_] if type_ in tmap else type_ + if get_child(node, 'DefaultButton') == 'true': + obj['defaultButton'] = True + if get_child(node, 'Check') == 'true': + obj['checked'] = True + set_command_picture(obj, node) + rep = get_child(node, 'Representation') + if rep: + obj['representation'] = rep + lic = get_child(node, 'LocationInCommandBar') + if lic: + obj['locationInCommandBar'] = lic + elif tag == 'ButtonGroup': + obj[key] = name + add_common_props(obj, node, name) + cs = get_child(node, 'CommandSource') + if cs: + if re.match(r'^\d+:[0-9a-fA-F]{8}-', cs): + sys.stderr.write("form-decompile: CommandSource '%s' (%s) — ссылка по id, не воспроизводима, опущена\n" % (cs, name)) + else: + obj['commandSource'] = cs + rep = get_child(node, 'Representation') + if rep: + obj['representation'] = rep + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'CommandBar': + obj[key] = name + add_common_props(obj, node, name) + cs = get_child(node, 'CommandSource') + if cs: + if re.match(r'^\d+:[0-9a-fA-F]{8}-', cs): + sys.stderr.write("form-decompile: CommandSource '%s' (%s) — ссылка по id, не воспроизводима, опущена\n" % (cs, name)) + else: + obj['commandSource'] = cs + hl = get_child(node, 'HorizontalLocation') + if hl: + obj['horizontalLocation'] = hl.lower() + if get_child(node, 'Autofill') == 'true': + obj['autofill'] = True + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'Popup': + obj[key] = name + add_common_props(obj, node, name) + set_command_picture(obj, node) + rep = get_child(node, 'Representation') + if rep: + obj['representation'] = rep + cs = get_child(node, 'CommandSource') + if cs: + if re.match(r'^\d+:[0-9a-fA-F]{8}-', cs): + sys.stderr.write("form-decompile: CommandSource '%s' (%s) — ссылка по id, не воспроизводима, опущена\n" % (cs, name)) + else: + obj['commandSource'] = cs + kids = decompile_children(node) + if kids: + obj['children'] = kids + elif tag == 'SearchStringAddition': + obj[key] = name + add_addition_core(obj, node, name) + elif tag == 'ViewStatusAddition': + obj[key] = name + add_addition_core(obj, node, name) + elif tag == 'SearchControlAddition': + obj[key] = name + add_addition_core(obj, node, name) + elif tag == 'SpreadSheetDocumentField': + decompile_simple_field(obj, node, name, key) + elif tag == 'HTMLDocumentField': + decompile_simple_field(obj, node, name, key) + elif tag == 'TextDocumentField': + decompile_simple_field(obj, node, name, key) + elif tag == 'FormattedDocumentField': + decompile_simple_field(obj, node, name, key) + elif tag == 'ProgressBarField': + decompile_simple_field(obj, node, name, key) + add_gauge_scalars(obj, node, ('MinValue', 'MaxValue')) + elif tag == 'TrackBarField': + decompile_simple_field(obj, node, name, key) + add_gauge_scalars(obj, node, ('MinValue', 'MaxValue', 'LargeStep', 'MarkingStep', 'Step')) + elif tag == 'ChartField': + decompile_simple_field(obj, node, name, key) + elif tag == 'GraphicalSchemaField': + decompile_simple_field(obj, node, name, key) + elif tag == 'PlannerField': + decompile_simple_field(obj, node, name, key) + elif tag == 'PeriodField': + decompile_simple_field(obj, node, name, key) + elif tag == 'DendrogramField': + decompile_simple_field(obj, node, name, key) + elif tag == 'GanttChartField': + decompile_simple_field(obj, node, name, key) + tbl_node = node.find('lf:Table', NS) + if tbl_node is not None: + obj['ganttTable'] = decompile_element(tbl_node) + + # DisplayImportance — атрибут открывающего тега, захват «как есть». + di = _attr(node, 'DisplayImportance') + if di: + obj['displayImportance'] = di + # title: "" — подавление авто-вывода компилятора там, где отсутствует. + if 'title' not in obj: + auto_title = False + if tag in ('LabelDecoration', 'Page', 'Popup'): + auto_title = True + elif tag == 'Button': + auto_title = not ('command' in obj or 'commandName' in obj or 'stdCommand' in obj) + elif tag in ('InputField', 'CheckBoxField', 'RadioButtonField', 'LabelField', 'Table', 'CalendarField'): + auto_title = 'path' not in obj + if auto_title: + obj['title'] = '' + add_layout(obj, node) + add_generic_scalars(obj, node) + # extendedTooltip: companion <ExtendedTooltip> (LabelDecoration). Текст-форма или own-content объект. + et_node = node.find('lf:ExtendedTooltip', NS) + if et_node is not None: + et_title = et_node.find('lf:Title', NS) + text_val = get_ml_formatted_value(et_title) if et_title is not None else None + et_obj = OrderedDict() + add_layout(et_obj, et_node) + add_generic_scalars(et_obj, et_node) + add_appearance(et_obj, et_node) + et_tt = et_node.find('lf:ToolTip', NS) + if et_tt is not None: + tt_val = get_lang_text(et_tt) + if tt_val is not None: + et_obj['tooltip'] = tt_val + if get_child(et_node, 'Visible') == 'false': + et_obj['hidden'] = True + if get_child(et_node, 'Enabled') == 'false': + et_obj['disabled'] = True + if get_child(et_node, 'Hyperlink') == 'true': + et_obj['hyperlink'] = True + et_ev = get_events(et_node, name) + if et_ev: + et_obj['events'] = et_ev + if len(et_obj) > 0: + if text_val is not None: + if isinstance(text_val, dict) and 'text' in text_val: + et_obj['text'] = text_val['text'] + if text_val['formatted']: + et_obj['formatted'] = True + else: + et_obj['text'] = text_val + if 'formatted' not in et_obj and et_title is not None and _attr(et_title, 'formatted') == 'true': + et_obj['formatted'] = True + obj['extendedTooltip'] = et_obj + elif text_val is not None: + obj['extendedTooltip'] = text_val + # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu + is_dyn_list_table = (tag == 'Table') and has_child(node, 'UpdateOnDataChange') + cb = decompile_companion_panel(node, 'AutoCommandBar', is_dyn_list_table) + if cb is not None: + obj['commandBar'] = cb + cm = decompile_companion_panel(node, 'ContextMenu') + if cm is not None: + obj['contextMenu'] = cm + return obj + + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите. +def pld_bool(v): + if v is None: + return None + return v == 'true' + + +def pld_int(v): + if v is None: + return None + return int(v) + + +def get_planner_value(node): + if node is None: + return None + if _attr(node, 'nil', NS_XSI) == 'true': + return None + t = _text(node) + if t: + return t + return None + + +def build_planner_font(node): + if node is None: + return None + o = OrderedDict() + for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + av = _attr(node, a) + if av != '': + o[a] = av + if len(o) == 0: + return None + return o + + +def build_planner_border(node): + if node is None: + return None + o = OrderedDict() + w = _attr(node, 'width') + if w != '': + o['width'] = int(w) + st = node.find('{*}style') + if st is not None: + o['style'] = _text(st) + return o + + +def build_planner_item(itn): + o = OrderedDict() + val_node = itn.find('{*}value') + if val_node is not None and _attr(val_node, 'nil', NS_XSI) != 'true' and _text(val_node): + o['value'] = _text(val_node) + o['text'] = get_child(itn, 'text') + tt = get_child(itn, 'tooltip') + if tt: + o['tooltip'] = tt + o['begin'] = get_child(itn, 'begin') + o['end'] = get_child(itn, 'end') + o['borderColor'] = get_child(itn, 'borderColor') + o['backColor'] = get_child(itn, 'backColor') + o['textColor'] = get_child(itn, 'textColor') + fnt = build_planner_font(itn.find('{*}font')) + if fnt: + o['font'] = fnt + o['replacementDate'] = get_child(itn, 'replacementDate') + o['deleted'] = pld_bool(get_child(itn, 'deleted')) + o['id'] = get_child(itn, 'id') + o['textFormatted'] = pld_bool(get_child(itn, 'textFormatted')) + brd = build_planner_border(itn.find('{*}border')) + if brd: + o['border'] = brd + o['editMode'] = get_child(itn, 'editMode') + return o + + +def build_planner_dim_element(eln): + o = OrderedDict() + v = get_planner_value(eln.find('{*}value')) + if v is not None: + o['value'] = v + o['text'] = get_child(eln, 'text') + o['borderColor'] = get_child(eln, 'borderColor') + o['backColor'] = get_child(eln, 'backColor') + o['textColor'] = get_child(eln, 'textColor') + fnt = build_planner_font(eln.find('{*}font')) + if fnt: + o['font'] = fnt + subs = [] + for s in eln.findall('{*}item'): + subs.append(build_planner_dim_element(s)) + if len(subs) > 0: + o['elements'] = subs + sos = get_child(eln, 'showOnlySubordinatesAreas') + if sos is not None: + o['showOnlySubordinatesAreas'] = (sos == 'true') + o['textFormatted'] = pld_bool(get_child(eln, 'textFormatted')) + return o + + +def build_planner_dimension(dn): + o = OrderedDict() + v = get_planner_value(dn.find('{*}value')) + if v is not None: + o['value'] = v + o['text'] = get_child(dn, 'text') + o['borderColor'] = get_child(dn, 'borderColor') + o['backColor'] = get_child(dn, 'backColor') + o['textColor'] = get_child(dn, 'textColor') + fnt = build_planner_font(dn.find('{*}font')) + if fnt: + o['font'] = fnt + els = [] + for e in dn.findall('{*}item'): + els.append(build_planner_dim_element(e)) + if len(els) > 0: + o['elements'] = els + o['textFormatted'] = pld_bool(get_child(dn, 'textFormatted')) + return o + + +def build_planner_level(lvn): + o = OrderedDict() + o['measure'] = get_child(lvn, 'measure') + o['interval'] = pld_int(get_child(lvn, 'interval')) + o['show'] = pld_bool(get_child(lvn, 'show')) + line_node = lvn.find('{*}line') + if line_node is not None: + ln = OrderedDict() + w = _attr(line_node, 'width') + if w != '': + ln['width'] = int(w) + g = _attr(line_node, 'gap') + if g != '': + ln['gap'] = (g == 'true') + st = line_node.find('{*}style') + if st is not None: + ln['style'] = _text(st) + o['line'] = ln + o['scaleColor'] = get_child(lvn, 'scaleColor') + o['dayFormatRule'] = get_child(lvn, 'dayFormatRule') + fmt_node = lvn.find('{*}format') + if fmt_node is not None: + f = get_lang_text(fmt_node) + if f is not None: + o['format'] = f + labels_node = lvn.find('{*}labels') + if labels_node is not None: + o['labels'] = OrderedDict([('ticks', pld_int(get_child(labels_node, 'ticks')))]) + o['backColor'] = get_child(lvn, 'backColor') + o['textColor'] = get_child(lvn, 'textColor') + o['showPereodicalLabels'] = pld_bool(get_child(lvn, 'showPereodicalLabels')) + return o + + +def build_planner_time_scale(tsn): + o = OrderedDict() + o['placement'] = get_child(tsn, 'placement') + levels = [] + for lvn in tsn.findall('{*}level'): + levels.append(build_planner_level(lvn)) + o['levels'] = levels + o['transparent'] = pld_bool(get_child(tsn, 'transparent')) + o['backColor'] = get_child(tsn, 'backColor') + o['textColor'] = get_child(tsn, 'textColor') + o['currentLevel'] = pld_int(get_child(tsn, 'currentLevel')) + return o + + +def build_planner_settings(set_node): + pl = OrderedDict() + item_nodes = set_node.findall('{*}item') + if len(item_nodes) > 0: + items = [] + for itn in item_nodes: + items.append(build_planner_item(itn)) + pl['items'] = items + dim_nodes = set_node.findall('{*}dimension') + if len(dim_nodes) > 0: + dims = [] + for dn in dim_nodes: + dims.append(build_planner_dimension(dn)) + pl['dimensions'] = dims + pl['borderColor'] = get_child(set_node, 'borderColor') + pl['backColor'] = get_child(set_node, 'backColor') + pl['textColor'] = get_child(set_node, 'textColor') + pl['lineColor'] = get_child(set_node, 'lineColor') + fnt = build_planner_font(set_node.find('{*}font')) + if fnt: + pl['font'] = fnt + pl['beginOfRepresentationPeriod'] = get_child(set_node, 'beginOfRepresentationPeriod') + pl['endOfRepresentationPeriod'] = get_child(set_node, 'endOfRepresentationPeriod') + pl['alignElementsOfTimeScale'] = pld_bool(get_child(set_node, 'alignElementsOfTimeScale')) + pl['displayTimeScaleWrapHeaders'] = pld_bool(get_child(set_node, 'displayTimeScaleWrapHeaders')) + pl['displayWrapHeaders'] = pld_bool(get_child(set_node, 'displayWrapHeaders')) + wf_node = set_node.find('{*}timeScaleWrapHeadersFormat') + if wf_node is not None: + wf = get_lang_text(wf_node) + if wf is not None: + pl['timeScaleWrapHeadersFormat'] = wf + pl['periodicVariantUnit'] = get_child(set_node, 'periodicVariantUnit') + pl['periodicVariantRepetition'] = pld_int(get_child(set_node, 'periodicVariantRepetition')) + pl['timeScaleWrapBeginIndent'] = pld_int(get_child(set_node, 'timeScaleWrapBeginIndent')) + pl['timeScaleWrapEndIndent'] = pld_int(get_child(set_node, 'timeScaleWrapEndIndent')) + ts_node = set_node.find('{*}timeScale') + if ts_node is not None: + pl['timeScale'] = build_planner_time_scale(ts_node) + per_node = set_node.find('{*}period') + if per_node is not None: + pl['period'] = OrderedDict([('begin', get_child(per_node, 'begin')), ('end', get_child(per_node, 'end'))]) + pl['displayCurrentDate'] = pld_bool(get_child(set_node, 'displayCurrentDate')) + pl['itemsTimeRepresentation'] = get_child(set_node, 'itemsTimeRepresentation') + pl['itemsBehaviorWhenSpaceInsufficient'] = get_child(set_node, 'itemsBehaviorWhenSpaceInsufficient') + pl['autoMinColumnWidth'] = pld_bool(get_child(set_node, 'autoMinColumnWidth')) + pl['autoMinRowHeight'] = pld_bool(get_child(set_node, 'autoMinRowHeight')) + pl['minColumnWidth'] = pld_int(get_child(set_node, 'minColumnWidth')) + pl['minRowHeight'] = pld_int(get_child(set_node, 'minRowHeight')) + pl['fixDimensionsHeader'] = get_child(set_node, 'fixDimensionsHeader') + pl['fixTimeScaleHeader'] = get_child(set_node, 'fixTimeScaleHeader') + brd = build_planner_border(set_node.find('{*}border')) + if brd: + pl['border'] = brd + pl['newItemsTextType'] = get_child(set_node, 'newItemsTextType') + return pl + + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart (генерик-движок). +CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'} +CHART_SERIES_FIELDS = {'realSeriesData', 'realExSeriesData', 'realPointData', 'realDataItems'} +CHART_ATTR_FIELDS = {'gaugeQualityBands'} + + +def conv_chart_scalar(v): + if v == 'true': + return True + if v == 'false': + return False + return v + + +def build_chart_node(n, name): + # ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени) + if name in CHART_ML_FIELDS: + ml = get_lang_text(n) + if ml is None: + return '' + return ml + kids = list(n) + if len(kids) == 0: + # лист: attrs-only (шрифт/gaugeQualityBands) или текст + attrs = [(k, v) for k, v in n.attrib.items() if k != _XSI_TYPE and k != ('{%s}nil' % NS_XSI)] + if len(attrs) > 0: + o = OrderedDict() + for k, v in attrs: + o[k] = conv_chart_scalar(v) + return o + return conv_chart_scalar(_text(n)) + # line/border: дочерний v8ui:style (+ width[/gap]) + style_child = n.find('{*}style') + if style_child is not None: + o = OrderedDict() + w = _attr(n, 'width') + if w != '': + o['width'] = int(w) + g = _attr(n, 'gap') + if g != '': + o['gap'] = (g == 'true') + o['style'] = _text(style_child) + return o + # вложенный объект d4p1: группируем детей по имени + o = OrderedDict() + for c in kids: + ln = _local_name(c.tag) + val = build_chart_node(c, ln) + if ln in CHART_SERIES_FIELDS: + if ln not in o: + o[ln] = [] + o[ln].append(val) + elif ln in o: + if not isinstance(o[ln], list): + o[ln] = [o[ln]] + o[ln].append(val) + else: + o[ln] = val + return o + + +def build_chart_settings(set_node): + return build_chart_node(set_node, '') + + +# Зеркало компилятор-эвристики B3: есть ли cmdBar-элемент где-либо в дереве. +def test_any_cmd_bar(lst): + if not lst: + return False + for e in lst: + if isinstance(e, dict) and 'cmdBar' in e: + return True + if isinstance(e, dict): + if 'children' in e and test_any_cmd_bar(e['children']): + return True + if 'columns' in e and test_any_cmd_bar(e['columns']): + return True + return False + + +# Объектный тип (зеркало Test-IsObjectLikeType) — кандидат на авто-main эвристики 11b.3. +def test_is_object_like_type_dec(type_): + if not type_: + return False + if type_ == 'DynamicList' or type_ == 'ConstantsSet': + return True + return bool(re.match(r'^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.', type_)) + + +# Ring 3: конструкции вне зоны поддержки (зеркало inline-скана канона 219-235). +def _ring3_scan(root, form_path): + # ConditionalAppearance со scope (привязка к области) пока не воспроизводим. + for ca in root.iter(): + if _local_name(ca.tag) != 'ConditionalAppearance': + continue + for item in list(ca): + if _local_name(item.tag) != 'item': + continue + for scope in list(item): + if _local_name(scope.tag) != 'scope': + continue + if len(scope) > 0 or (scope.text or '').strip(): + fail_ring3("ConditionalAppearance со scope", "form/ConditionalAppearance/item/scope") + # Реквизит с design-time <Settings> chart-типа (кроме TypeDescription/DynamicList/Planner/Chart). + for attr in root.iter(): + if _local_name(attr.tag) != 'Attribute': + continue + for s in list(attr): + if _local_name(s.tag) != 'Settings': + continue + st = _attr(s, 'type', NS_XSI) + if st and not re.search(r'TypeDescription$', st) and not re.search(r'DynamicList$', st) and not re.search(r'Planner$', st) and not re.search(r'd4p1:(Gantt)?Chart$', st): + fail_ring3("Attribute>Settings типа '%s' (design-time конфигурация, напр. диаграмма)" % st, "Attribute/Settings") + elif re.search(r'd4p1:(Gantt)?Chart$', st): + # Chart/GanttChart с типизированными значениями/осями/точками (d4p1 xsi:type/nil/item). + chart_uri = NSMAP_DOC.get('d4p1') + typed = False + if chart_uri: + pref = '{%s}' % chart_uri + nil_key = '{%s}nil' % NS_XSI + for d in s.iter(): + if not d.tag.startswith(pref): + continue + if _XSI_TYPE in d.attrib or nil_key in d.attrib or _local_name(d.tag) == 'item': + typed = True + break + if typed: + fail_ring3("Attribute>Settings %s с точками/осями (типизированные значения/d4p1-ML)" % st, "Attribute/Settings") + + +# ═══════════════════════════════════════════════════════════════════════════ +# MAIN +# ═══════════════════════════════════════════════════════════════════════════ + +def main(): + global ROOT, NSMAP_DOC, OUTPUT_DIR, OUTPUT_BASENAME, QUERY_FILES_ACCUMULATOR, QUERY_FILE_NAMES_USED + parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False) + parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True) + parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None) + args = parser.parse_args() + + form_path = args.FormPath + output_path = args.OutputPath + + # --- 0. Resolve and validate input --- + if not os.path.exists(form_path): + sys.stderr.write("Form not found: %s\n" % form_path) + sys.exit(1) + form_path = os.path.abspath(form_path) + + # Префикс→URI (для ring3-проверки d4p1-чартов). Первое связывание на префикс. + NSMAP_DOC = {} + for _ev, (_pfx, _uri) in ET.iterparse(form_path, events=['start-ns']): + if _pfx not in NSMAP_DOC: + NSMAP_DOC[_pfx] = _uri + + # .NET XmlDocument сохраняет литеральные \r\n в InnerText; ET (expat) нормализует CRLF→LF + # по XML-спеке. Чтобы совпасть с каноном, превращаем \r\n→ \n (символьные ссылки не + # подлежат EOL-нормализации) в содержимом документа — но только от <Form (в прологе char-ref + # невалиден). Покрывает многострочные title/tooltip/content/QueryText. + raw = open(form_path, 'rb').read() + rs = raw.find(b'<Form') + re_end = raw.rfind(b'</Form>') + if rs >= 0 and re_end >= 0: + end = re_end + len(b'</Form>') + # Только ВНУТРИ корневого элемента: в прологе/эпилоге char-ref невалиден (хвостовой </Form>\r\n). + raw = raw[:rs] + raw[rs:end].replace(b'\r\n', b' \n') + raw[end:] + root = ET.fromstring(raw) + ROOT = root + + # Ring 2: not a managed Form + if _local_name(root.tag) != 'Form': + sys.stderr.write("form-decompile: корневой элемент <%s> не <Form> — это не управляемая форма.\n" % _local_name(root.tag)) + sys.exit(2) + + # --- Вынос запроса динсписка в .sql рядом с output --- + QUERY_FILES_ACCUMULATOR = [] + QUERY_FILE_NAMES_USED = {} + if output_path: + od = os.path.dirname(output_path) + if not od: + od = os.getcwd() + OUTPUT_DIR = od + OUTPUT_BASENAME = os.path.splitext(os.path.basename(output_path))[0] + + # --- 1b. Ring-3 scan --- + _ring3_scan(root, form_path) + + # --- 5. Form-level assembly --- + dsl = OrderedDict() + + title_node = root.find('lf:Title', NS) + if title_node is not None: + t = get_lang_text(title_node) + if t is not None: + dsl['title'] = t + + # properties (прямые скаляры под <Form>, PascalCase → camelCase) + known_form_props = ['AutoTitle', 'ReportResult', 'DetailsData', 'ReportFormType', 'AutoShowState', 'ReportResultViewMode', 'ViewModeApplicationOnSetReportResult', 'WindowOpeningMode', 'CommandBarLocation', 'SaveDataInSettings', 'AutoSaveDataInSettings', 'AutoTime', 'UsePostingMode', 'RepostOnWrite', 'AutoURL', 'AutoFillCheck', 'Customizable', 'EnterKeyBehavior', 'VerticalScroll', 'Width', 'Height', 'Group', 'UseForFoldersAndItems', 'SaveWindowSettings', 'ScalingMode', 'VerticalSpacing', 'VariantAppearance', 'ShowCloseButton', 'HorizontalAlign', 'ChildrenAlign', 'ShowTitle', 'ConversationsRepresentation', 'CollapseItemsByImportanceVariant', 'GroupList', 'ChildItemsWidth', 'VerticalAlign', 'HorizontalSpacing', 'CustomSettingsFolder', 'SettingsStorage', 'Enabled', 'Scale'] + props = OrderedDict() + for pn in known_form_props: + v = get_child(root, pn) + if v is not None: + camel = pn[0:1].lower() + pn[1:] + if v == 'true': + props[camel] = True + elif v == 'false': + props[camel] = False + elif re.match(r'^\d+$', v): + props[camel] = int(v) + else: + props[camel] = v + # Ссылка на члена формы по id ("N:uuid") в groupList/customSettingsFolder НЕ воспроизводима. + for ref_key in ('groupList', 'customSettingsFolder'): + if ref_key in props and re.match(r'^\d+:[0-9a-fA-F]{8}-', _ps_str(props[ref_key])): + sys.stderr.write("form-decompile: %s = '%s' — ссылка на члена формы по id, не воспроизводима (id переназначаются), опущена. Задайте по имени через form-edit.\n" % (ref_key, props[ref_key])) + del props[ref_key] + # AutoTitle при наличии title: компилятор инъектит false (~95%). Зеркалим. + if 'title' in dsl: + if 'autoTitle' not in props: + props['autoTitle'] = '' + elif props['autoTitle'] is False: + del props['autoTitle'] + if len(props) > 0: + dsl['properties'] = props + + # MobileDeviceCommandBarContent (form-level) → список имён + mdcb = root.find('lf:MobileDeviceCommandBarContent', NS) + if mdcb is not None: + names = [] + for it in mdcb.findall('xr:Item', NS): + v = it.find('xr:Value', NS) + if v is not None: + names.append(_text(v)) + if len(names) > 0: + dsl['mobileCommandBarContent'] = names + + # excludedCommands (form-level <CommandSet>) + cs_form = root.find('lf:CommandSet', NS) + if cs_form is not None: + exc_form = [] + for ec in cs_form.findall('lf:ExcludedCommand', NS): + exc_form.append(_text(ec)) + if len(exc_form) > 0: + dsl['excludedCommands'] = exc_form + + # events (form-level) → {Event: handler} напрямую + ev_form = get_events(root, None) + if ev_form: + ev_map = OrderedDict() + ev_node = root.find('lf:Events', NS) + for e in ev_node.findall('lf:Event', NS): + ev_map[_attr(e, 'name')] = _text(e) + if len(ev_map) > 0: + dsl['events'] = ev_map + + # elements (+ форменный AutoCommandBar как autoCmdBar-элемент) + elem_list = [] + elements = decompile_children(root) + form_has_cmd_bar = test_any_cmd_bar(elements) + acb = root.find('lf:AutoCommandBar', NS) + if acb is not None: + haln = get_child(acb, 'HorizontalAlign') + acb_autofill = get_child(acb, 'Autofill') + acb_di = _attr(acb, 'DisplayImportance') + acb_kids = decompile_children(acb) + acb_obj = None + if haln or (acb_autofill == 'false') or acb_kids or acb_di: + acb_obj = OrderedDict() + acb_obj['autoCmdBar'] = _attr(acb, 'name') + if acb_di: + acb_obj['displayImportance'] = acb_di + if haln: + acb_obj['horizontalAlign'] = haln + if acb_autofill == 'false': + acb_obj['autofill'] = False + if acb_kids: + acb_obj['children'] = acb_kids + elif form_has_cmd_bar and acb_autofill is None: + acb_obj = OrderedDict() + acb_obj['autoCmdBar'] = _attr(acb, 'name') + acb_obj['autofill'] = True + if acb_obj is not None: + elem_list.append(acb_obj) + if elements: + for e in elements: + elem_list.append(e) + if len(elem_list) > 0: + dsl['elements'] = elem_list + + # attributes + attrs_node = root.find('lf:Attributes', NS) + if attrs_node is not None: + attrs = [] + # Подавление авто-main (эвристика 11b.3): нет <MainAttribute> И ровно 1 объектный реквизит. + all_attr_nodes = attrs_node.findall('lf:Attribute', NS) + any_main_attr = False + obj_like_nodes = [] + for an in all_attr_nodes: + if get_child(an, 'MainAttribute') == 'true': + any_main_attr = True + atype = decompile_type(an.find('lf:Type', NS)) + if test_is_object_like_type_dec(_ps_str(atype)): + obj_like_nodes.append(an) + suppress_main_name = _attr(obj_like_nodes[0], 'name') if (not any_main_attr and len(obj_like_nodes) == 1) else None + for a in all_attr_nodes: + ao = OrderedDict() + ao['name'] = _attr(a, 'name') + ty = decompile_type(a.find('lf:Type', NS)) + if ty: + ao['type'] = ty + # valueType: <Settings xsi:type="v8:TypeDescription">; Planner/Chart — отдельно. + set_node = a.find('lf:Settings', NS) + if set_node is not None and re.search(r'TypeDescription$', _attr(set_node, 'type', NS_XSI)): + vt = decompile_type(set_node) + ao['valueType'] = vt if vt else '' + elif set_node is not None and re.search(r'Planner$', _attr(set_node, 'type', NS_XSI)): + ao['planner'] = build_planner_settings(set_node) + elif set_node is not None and re.search(r'd4p1:(Gantt)?Chart$', _attr(set_node, 'type', NS_XSI)): + ao['chart'] = build_chart_settings(set_node) + if get_child(a, 'MainAttribute') == 'true': + ao['main'] = True + elif suppress_main_name and ao['name'] == suppress_main_name: + ao['main'] = False + vw = decompile_xr_flag(a, 'View') + if vw is not None: + ao['view'] = vw + ed = decompile_xr_flag(a, 'Edit') + if ed is not None: + ao['edit'] = ed + is_main = (ao.get('main') is True) + t_node = a.find('lf:Title', NS) + if t_node is not None: + t = get_lang_text_ws(t_node) + if t is not None: + if is_main or not isinstance(t, str) or not _ps_ieq(t, title_from_name(ao['name'])): + ao['title'] = t + elif not is_main: + ao['title'] = '' + if get_child(a, 'SavedData') == 'true': + ao['savedData'] = True + elif ao.get('main') is True and re.search(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.', _ps_str(ao.get('type'))): + ao['savedData'] = False + save_node = a.find('lf:Save', NS) + if save_node is not None: + nm = _ps_str(ao['name']) + flds = [_text(f) for f in save_node.findall('lf:Field', NS)] + if len(flds) == 1 and flds[0] == nm: + ao['save'] = True + elif len(flds) > 0: + stripped = [] + for f in flds: + m = re.match('^' + re.escape(nm) + r'\.([^.]+)$', f) + if m and not re.match(r'^\d+/\d+', m.group(1)): + stripped.append(m.group(1)) + else: + stripped.append(f) + ao['save'] = stripped[0] if len(stripped) == 1 else stripped + fc = get_child(a, 'FillCheck') + if fc: + ao['fillCheck'] = fc + afo = decompile_functional_options(a) + if _ps_truthy(afo): + ao['functionalOptions'] = afo + cols_node = a.find('lf:Columns', NS) + if cols_node is not None: + cols = [] + for c in cols_node.findall('lf:Column', NS): + cols.append(decompile_attr_column(c)) + if len(cols) > 0: + ao['columns'] = cols + add_nodes = cols_node.findall('lf:AdditionalColumns', NS) + if len(add_nodes) > 0: + add_list = [] + for an2 in add_nodes: + ac_obj = OrderedDict() + ac_obj['table'] = _attr(an2, 'table') + ac_cols = [] + for c in an2.findall('lf:Column', NS): + ac_cols.append(decompile_attr_column(c)) + ac_obj['columns'] = ac_cols + add_list.append(ac_obj) + ao['additionalColumns'] = add_list + # UseAlways: префикс "ИмяРеквизита." снимаем; маркер "~" сохраняем (префикс после него). + ua_node = a.find('lf:UseAlways', NS) + if ua_node is not None: + prefix = _ps_str(ao['name']) + '.' + shorts = [] + for fn in ua_node.findall('lf:Field', NS): + t = (_text(fn) or '').strip() + if t.startswith('~'): + rest = t[1:] + if rest.startswith(prefix): + rest = rest[len(prefix):] + t = '~' + rest + elif t.startswith(prefix): + t = t[len(prefix):] + shorts.append(t) + if 'columns' in ao: + rest_list = [] + for s in shorts: + col = None + for cc in ao['columns']: + if cc.get('name') == s: + col = cc + break + if col is not None: + col['useAlways'] = True + else: + rest_list.append(s) + if len(rest_list) > 0: + ao['useAlways'] = rest_list + elif len(shorts) > 0: + ao['useAlways'] = shorts + # Settings динамического списка (xsi:type=DynamicList) + set_node = a.find('lf:Settings', NS) + if set_node is not None and re.search(r'DynamicList$', _attr(set_node, 'type', NS_XSI)): + so = OrderedDict() + afaf = get_child(set_node, 'AutoFillAvailableFields') + if afaf is not None: + so['autoFillAvailableFields'] = (afaf == 'true') + mt = get_child(set_node, 'MainTable') + if mt: + so['mainTable'] = mt + gifp = get_child(set_node, 'GetInvisibleFieldPresentations') + if gifp is not None: + so['getInvisibleFieldPresentations'] = (gifp == 'true') + kt = get_child(set_node, 'KeyType') + if kt: + so['keyType'] = kt + kf_nodes = [_text(x) for x in set_node.findall('lf:KeyField', NS)] + if len(kf_nodes) > 0: + so['keyFields'] = kf_nodes + asus = get_child(set_node, 'AutoSaveUserSettings') + if asus is not None: + so['autoSaveUserSettings'] = (asus == 'true') + qt_node = set_node.find('lf:QueryText', NS) + has_q = bool(qt_node is not None and _text(qt_node)) + if has_q: + so['query'] = maybe_externalize_query(_text(qt_node), _ps_str(ao['name'])) + mq_v = get_child(set_node, 'ManualQuery') + if mq_v is not None: + mq_actual = (mq_v == 'true') + if mq_actual != has_q: + so['manualQuery'] = mq_actual + if get_child(set_node, 'DynamicDataRead') == 'false': + so['dynamicDataRead'] = False + field_nodes = set_node.findall('lf:Field', NS) + if len(field_nodes) > 0: + fields = [] + for fn in field_nodes: + fo = OrderedDict() + fld = get_child(fn, 'field') + dp = get_child(fn, 'dataPath') + if fld: + fo['field'] = fld + if has_child(fn, 'dataPath') and dp != fld: + fo['dataPath'] = dp + f_type_attr = _attr(fn, 'type', NS_XSI) + if re.search(r'NestedDataSet$', f_type_attr): + fo['nested'] = True + elif re.search(r'Folder$', f_type_attr): + fo['folder'] = True + ftn = fn.find('dcssch:title', NS) + if ftn is not None: + t = get_lang_text(ftn) + if t is not None: + fo['title'] = t + fvt = fn.find('dcssch:valueType', NS) + if fvt is not None: + fvt_val = decompile_type(fvt) + if fvt_val: + fo['valueType'] = fvt_val + fpe = get_child(fn, 'presentationExpression') + if fpe is not None and fpe != '': + fo['presentationExpression'] = fpe + fapp_node = fn.find('dcssch:appearance', NS) + if fapp_node is not None: + fap = get_settings_appearance(fapp_node) + if fap and len(fap) > 0: + fo['appearance'] = fap + fur_node = fn.find('dcssch:useRestriction', NS) + if fur_node is not None: + fur = build_restrict_obj(fur_node) + if len(fur) > 0: + fo['useRestriction'] = fur + faur_node = fn.find('dcssch:attributeUseRestriction', NS) + if faur_node is not None: + faur = build_restrict_obj(faur_node) + if len(faur) > 0: + fo['attributeUseRestriction'] = faur + fip_node = fn.find('dcssch:inputParameters', NS) + if fip_node is not None: + fip = build_dl_input_parameters(fip_node) + if len(fip) > 0: + fo['inputParameters'] = fip[0] if len(fip) == 1 else fip # PS unwrap + fields.append(fo) + so['fields'] = fields + calc_nodes = set_node.findall('lf:CalculatedField', NS) + if len(calc_nodes) > 0: + cfs = [] + for cn in calc_nodes: + cfs.append(build_calc_field(cn)) + so['calculatedFields'] = cfs + param_nodes = set_node.findall('lf:Parameter', NS) + if len(param_nodes) > 0: + dl_pars = [] + for pn in param_nodes: + dl_pars.append(build_dl_parameter(pn)) + so['parameters'] = dl_pars + ls_node = set_node.find('lf:ListSettings', NS) + if ls_node is not None: + f_node = ls_node.find('dcsset:filter', NS) + if f_node is not None and f_node.find('dcsset:item', NS) is not None: + flt = [] + for fc in f_node.findall('dcsset:item', NS): + bi = build_filter_item(fc, 'settings/filter') + if bi is not None: + flt.append(bi) + if len(flt) > 0: + so['filter'] = flt + o_node = ls_node.find('dcsset:order', NS) + if o_node is not None and o_node.find('dcsset:item', NS) is not None: + ordr = build_order(o_node, 'settings/order') + if len(ordr) > 0: + so['order'] = ordr + ca_node = ls_node.find('dcsset:conditionalAppearance', NS) + if ca_node is not None and ca_node.find('dcsset:item', NS) is not None: + ca = build_conditional_appearance(ca_node, 'settings/conditionalAppearance') + if len(ca) > 0: + so['conditionalAppearance'] = ca + dp_node = ls_node.find('dcsset:dataParameters', NS) + if dp_node is not None and dp_node.find('dcscor:item', NS) is not None: + dp = build_form_data_parameters(dp_node) + if len(dp) > 0: + so['dataParameters'] = dp + grp_item_node = ls_node.find('dcsset:item', NS) + grouping = None + if grp_item_node is not None: + grouping = build_list_grouping(grp_item_node) + if grouping is not None: + so['grouping'] = grouping + ls_shape = get_list_settings_shape(ls_node, grouping is not None) + if ls_shape is not None: + so['listSettings'] = ls_shape + if len(so) > 0: + ao['settings'] = so + attrs.append(ao) + if len(attrs) > 0: + dsl['attributes'] = attrs + + # conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>) + if attrs_node is not None: + ca_node = attrs_node.find('lf:ConditionalAppearance', NS) + if ca_node is not None: + ca = build_conditional_appearance(ca_node, 'form/conditionalAppearance') + if len(ca) > 0: + dsl['conditionalAppearance'] = ca + + # parameters + pars_node = root.find('lf:Parameters', NS) + if pars_node is not None: + pars = [] + for p in pars_node.findall('lf:Parameter', NS): + po = OrderedDict() + po['name'] = _attr(p, 'name') + ty = decompile_type(p.find('lf:Type', NS)) + if ty: + po['type'] = ty + if get_child(p, 'KeyParameter') == 'true': + po['key'] = True + pars.append(po) + if len(pars) > 0: + dsl['parameters'] = pars + + # commands + cmds_node = root.find('lf:Commands', NS) + if cmds_node is not None: + cmds = [] + for c in cmds_node.findall('lf:Command', NS): + co = OrderedDict() + co['name'] = _attr(c, 'name') + act = get_child(c, 'Action') + if act: + co['action'] = act + if get_child(c, 'ModifiesSavedData') == 'true': + co['modifiesSavedData'] = True + t_node = c.find('lf:Title', NS) + if t_node is not None: + t = get_lang_text(t_node) + if t is not None: + co['title'] = t + else: + co['title'] = '' + tt_node = c.find('lf:ToolTip', NS) + if tt_node is not None: + t = get_lang_text(tt_node) + if t is not None: + co['tooltip'] = t + us = decompile_xr_flag(c, 'Use') + if us is not None: + co['use'] = us + cfo = decompile_functional_options(c) + if _ps_truthy(cfo): + co['functionalOptions'] = cfo + cru = get_child(c, 'CurrentRowUse') + if cru: + co['currentRowUse'] = cru + ate = get_child(c, 'AssociatedTableElementId') + if ate: + co['table'] = ate + sc = get_child(c, 'Shortcut') + if sc: + co['shortcut'] = sc + set_command_picture(co, c) + rep = get_child(c, 'Representation') + if rep: + co['representation'] = rep + cmds.append(co) + if len(cmds) > 0: + dsl['commands'] = cmds + + # commandInterface (форменный <CommandInterface>) + ci = decompile_command_interface() + if ci is not None: + dsl['commandInterface'] = ci + + # --- 6. Output --- + js = convert_to_compact_json(dsl) + if output_path: + with open(output_path, 'w', encoding='utf-8', newline='') as f: + f.write(js) + save_query_files() + sys.stdout.write("form-decompile: %s\n" % output_path) + else: + sys.stdout.write(js + "\n") + + +def _local_name(tag): + """{uri}local → local.""" + return tag.rsplit('}', 1)[-1] if '}' in tag else tag + + +if __name__ == '__main__': + main()