mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-14 18:04:58 +03:00
3624 lines
148 KiB
Python
3624 lines
148 KiB
Python
#!/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 → вынести в `<basename>-<listName>.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
|
||
|
||
|
||
# Группировка строк динамического списка: цепочка <dcsset:item StructureItemGroup>.
|
||
# Плоский массив уровней ИЛИ $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 <v8:content>: 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
|
||
|
||
|
||
# Прочитать <dcscor:value> в 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 ≠ отсутствие <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 из <dcsset:appearance> (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()
|
||
|
||
|
||
# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика }.
|
||
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
|
||
|
||
|
||
# Командный интерфейс формы (<CommandInterface>): панели 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
|
||
|
||
|
||
# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс снят; 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
|
||
|
||
|
||
# <Picture> кнопки/попапа/команды. Дефолт 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
|
||
|
||
|
||
# Шрифт <Font ...> → строка-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
|
||
|
||
|
||
# Граница <Border> → строка-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 — список отключённых команд редактора (только <ExcludedCommand>).
|
||
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 динамического списка (<CalculatedField>) → объектная модель.
|
||
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
|
||
|
||
|
||
# Параметры выбора входа дин-списка (<inputParameters>) → массив. Зеркало 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) — все <dcscor:value> массивом
|
||
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: <ChoiceList><xr:Item>… → [ { 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-значение — компилятор эмитит <Value xsi:nil="true"/>.
|
||
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: непустой → текст; пустой <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
|
||
|
||
|
||
# Значение Параметра выбора (<lf:Value>): скаляр или 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: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{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: <TypeLink><xr:DataPath><xr:LinkItem> → {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
|
||
|
||
|
||
# Захват <Format>/<EditFormat> (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': # тег <Hiperlink> (опечатка платформы)
|
||
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: "" — подавление авто-вывода компилятора там, где <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()
|