From fea2f37ba6a174630fe53ac5a0f51496105b2813 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 25 May 2026 12:15:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd-decompile):=20Python-=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=20=D0=B7=D0=B5=D1=80=D0=BA=D0=B0=D0=BB=D0=BE=D0=BC?= =?UTF-8?q?=20PS=20v0.88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зеркало skd-decompile.ps1 v0.88 (~3022 строки) → skd-decompile.py v0.88 (~3140 строк). По образцу пары skd-compile.ps1 ↔ skd-compile.py. - runner.mjs --filter skd-decompile --runtime python: 16/16 зелёные - runner.mjs --filter skd-decompile (PS, регрессия): 16/16 - runner.mjs --filter skd-compile --runtime python (регрессия): 23/23 - verify-roundtrip на titan2-subset (13 отчётов): PS ≡ Py байт-в-байт - verify-roundtrip на sample30 (20 общих отчётов): тот же распред 8 BP + 12 diff, у Py чуть меньше diff-строк на edge-кейсах empty multilang content Нетривиальные места порта: - ET в Python не понимает prefix-aware XPath → тонкая обёртка XNode + ручной _xpath_steps/_all/_single для PS-style путей - ET.Element (C-impl) не позволяет навешивать атрибуты → per-element nsmap хранится во внешнем _NSMAP_BY_ID[id(el)], заполняется через iterparse + start-ns - JSON-сериализатор (convert_to_compact_json, try_inline_json, lineLimit=400, inline-when-fits) портирован 1-в-1 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skd-decompile/scripts/skd-decompile.py | 3142 +++++++++++++++++ 1 file changed, 3142 insertions(+) create mode 100644 .claude/skills/skd-decompile/scripts/skd-decompile.py diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.py b/.claude/skills/skd-decompile/scripts/skd-decompile.py new file mode 100644 index 00000000..c38a3f94 --- /dev/null +++ b/.claude/skills/skd-decompile/scripts/skd-decompile.py @@ -0,0 +1,3142 @@ +#!/usr/bin/env python3 +# skd-decompile v0.88 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +import argparse +import os +import re +import sys +import xml.etree.ElementTree as ET + +# --- 1. Namespace manager --- + +NS_SCHEMA = "http://v8.1c.ru/8.1/data-composition-system/schema" +NS_COM = "http://v8.1c.ru/8.1/data-composition-system/common" +NS_COR = "http://v8.1c.ru/8.1/data-composition-system/core" +NS_SET = "http://v8.1c.ru/8.1/data-composition-system/settings" +NS_AT = "http://v8.1c.ru/8.1/data-composition-system/area-template" +NS_V8 = "http://v8.1c.ru/8.1/data/core" +NS_V8UI = "http://v8.1c.ru/8.1/data/ui" +NS_XS = "http://www.w3.org/2001/XMLSchema" +NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" +NS_CFG = "http://v8.1c.ru/8.1/data/enterprise/current-config" + +NS_PREFIX_MAP = { + 'r': NS_SCHEMA, + 'dcscom': NS_COM, + 'dcscor': NS_COR, + 'dcsset': NS_SET, + 'dcsat': NS_AT, + 'v8': NS_V8, + 'v8ui': NS_V8UI, + 'xs': NS_XS, + 'xsi': NS_XSI, +} + +_NSMAP_BY_ID = {} # id(Element) -> dict[prefix, uri] of locally declared xmlns + + +def _get_nsmap(el): + return _NSMAP_BY_ID.get(id(el), {}) + + +# Element wrapper: holds ET element + per-element prefix→uri map (inherited). +class XNode: + __slots__ = ('el', 'nsmap', 'parent') + + def __init__(self, el, nsmap, parent=None): + self.el = el + self.nsmap = nsmap # dict: prefix → uri (in scope) + self.parent = parent + + def __bool__(self): + return self.el is not None + + @property + def local_name(self): + t = self.el.tag + if '}' in t: + return t.split('}', 1)[1] + return t + + @property + def namespace_uri(self): + t = self.el.tag + if t.startswith('{'): + return t[1:].split('}', 1)[0] + return '' + + @property + def inner_text(self): + # Concatenate all text content (mirror PS InnerText) + return _inner_text(self.el) + + @property + def has_child_nodes(self): + return len(self.el) > 0 or (self.el.text is not None and self.el.text != '') + + @property + def child_nodes(self): + for sub in self.el: + yield XNode(sub, _merged_nsmap(self.nsmap, sub), self) + + def get_namespace_of_prefix(self, prefix): + return self.nsmap.get(prefix) + + def get_attribute(self, name, ns_uri=None): + if ns_uri: + v = self.el.get('{%s}%s' % (ns_uri, name)) + return v if v is not None else '' + # No namespace — try plain + v = self.el.get(name) + return v if v is not None else '' + + def get_attr_raw(self, qname): + # qname like "xsi:type" — get raw attribute by qname value (we need to lookup ns) + if ':' in qname: + pfx, local = qname.split(':', 1) + uri = self.nsmap.get(pfx) + if uri: + v = self.el.get('{%s}%s' % (uri, local)) + if v is not None: + return v + return None + v = self.el.get(qname) + return v + + def select_single_node(self, xpath): + return _xpath_single(self, xpath) + + def select_nodes(self, xpath): + return _xpath_all(self, xpath) + + +# Build nsmap from xmlns attributes on the element + parent inheritance. +def _merged_nsmap(parent_map, el): + nsmap = dict(parent_map) + # ET parsed elements lose xmlns attrs but expose tag namespaces via {uri}. + # We pre-populate via iterparse — see parse_xml below. + extra = _get_nsmap(el) + if extra: + nsmap.update(extra) + return nsmap + + +def _inner_text(el): + # All text content concatenated + parts = [] + if el.text: + parts.append(el.text) + for child in el: + parts.append(_inner_text(child)) + if child.tail: + parts.append(child.tail) + return ''.join(parts) + + +def parse_xml(path): + # Use iterparse to capture start-ns events and attach nsmap to each element. + ns_stack = [{}] + pending_ns = {} + root = None + parents_stack = [] + parser = ET.iterparse(path, events=('start', 'end', 'start-ns')) + for event, item in parser: + if event == 'start-ns': + prefix, uri = item + pending_ns[prefix if prefix else ''] = uri + elif event == 'start': + # item is the element + if pending_ns: + _NSMAP_BY_ID[id(item)] = pending_ns + pending_ns = {} + else: + _NSMAP_BY_ID[id(item)] = {} + if root is None: + root = item + elif event == 'end': + pass + return root + + +# XPath translator: parse PS-style XPath like "r:name", "dcsset:item", "dcsset:item[@xsi:type='dcsset:TableRow']" +# Returns list of (axis, pred) tokens. axis = ('child', uri, local) or ('descendant', uri, local). +def _xpath_steps(xpath): + # Split by '/' but not inside [] — predicates here don't include /. + steps = [] + parts = xpath.split('/') + i = 0 + while i < len(parts): + p = parts[i] + if p == '': + # '//' → next part is descendant + i += 1 + if i < len(parts): + steps.append(('descendant', parts[i])) + i += 1 + continue + steps.append(('child', p)) + i += 1 + return steps + + +def _step_match(step_text): + # Returns (qname, predicate) + pred = None + m = re.match(r'^([^\[]+)(\[.+\])?$', step_text) + if not m: + return step_text, None + qname = m.group(1) + pred = m.group(2) + return qname, pred + + +def _qname_to_tag(qname, nsmap): + # qname: "prefix:local" or "*[local-name()='x']" handling + if qname == '*': + return None, None + # Wildcard with local-name predicate handled by caller + if ':' in qname: + pfx, local = qname.split(':', 1) + uri = nsmap.get(pfx) + if uri: + return uri, local + return None, local + return None, qname + + +def _check_predicate(node, pred): + if not pred: + return True + # Only support [@xsi:type='value'] form + m = re.match(r"^\[@([^=]+)=['\"]([^'\"]+)['\"]\]$", pred) + if m: + attr = m.group(1) + expected = m.group(2) + actual = node.get_attr_raw(attr) + if actual is None: + return False + return actual == expected + # Fallback: ignore unknown predicates (treat as match) + return True + + +def _xpath_all(node, xpath): + steps = _xpath_steps(xpath) + current = [node] + for axis, step_text in steps: + qname, pred = _step_match(step_text) + next_nodes = [] + # Handle local-name() form + if qname.startswith("*[local-name()="): + mlc = re.match(r"^\*\[local-name\(\)=['\"]([^'\"]+)['\"]\]$", qname) + target_local = mlc.group(1) if mlc else None + for n in current: + if axis == 'child': + for child in n.el: + wrapped = XNode(child, _merged_nsmap(n.nsmap, child), n) + if wrapped.local_name == target_local and _check_predicate(wrapped, pred): + next_nodes.append(wrapped) + else: # descendant + for child in n.el.iter(): + if child is n.el: + continue + wrapped = XNode(child, _merged_nsmap(n.nsmap, child), n) + if wrapped.local_name == target_local and _check_predicate(wrapped, pred): + next_nodes.append(wrapped) + current = next_nodes + continue + + uri, local = _qname_to_tag(qname, node.nsmap) + for n in current: + if axis == 'child': + children_iter = list(n.el) + else: + children_iter = [c for c in n.el.iter() if c is not n.el] + for child in children_iter: + wrapped = XNode(child, _merged_nsmap(n.nsmap, child), n) + # Match local + uri + if wrapped.local_name != local: + continue + if uri is not None and wrapped.namespace_uri != uri: + continue + if not _check_predicate(wrapped, pred): + continue + next_nodes.append(wrapped) + current = next_nodes + return current + + +def _xpath_single(node, xpath): + res = _xpath_all(node, xpath) + if res: + return res[0] + return None + + +# --- 0. Resolve and validate input --- + +def main(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser(description='Decompile 1C DCS Template.xml to JSON DSL', allow_abbrev=False) + parser.add_argument('-TemplatePath', '-Path', dest='TemplatePath', type=str, required=True) + parser.add_argument('-OutputPath', type=str, default=None) + args = parser.parse_args() + + global TemplatePath, OutputPath + TemplatePath = args.TemplatePath + OutputPath = args.OutputPath + + if not os.path.exists(TemplatePath): + print(f"Template not found: {TemplatePath}", file=sys.stderr) + sys.exit(1) + + TemplatePath = os.path.abspath(TemplatePath) + + _NSMAP_BY_ID.clear() + root_el = parse_xml(TemplatePath) + root = XNode(root_el, dict(_get_nsmap(root_el)), None) + + # Ring 3: not a DataCompositionSchema → fail-fast + if root.local_name != 'DataCompositionSchema': + print(f"skd-decompile: корневой элемент <{root.local_name}> не — это не схема СКД (возможно, табличный документ — используй /mxl-decompile).", file=sys.stderr) + sys.exit(2) + + run(root) + + +# --- 1b. Ring 3 scan: bail out on unsupported constructs --- + +def fail_ring3(kind, loc): + print(f"skd-decompile: декомпиляция не поддерживает {kind} (path: {loc})", file=sys.stderr) + print("Для точечной работы с этим отчётом используй /skd-edit.", file=sys.stderr) + sys.exit(3) + + +# --- 2. Warnings accumulator --- + +warnings_list = [] +warning_counter = [0] + + +def add_warning(kind, loc, detail): + warning_counter[0] += 1 + wid = "W%03d" % warning_counter[0] + warnings_list.append({'id': wid, 'kind': kind, 'loc': loc, 'detail': detail}) + return wid + + +def new_sentinel(kind, loc, detail): + wid = add_warning(kind, loc, detail) + return {'__unsupported__': {'id': wid, 'kind': kind, 'loc': loc}} + + +# --- 3. Helpers --- + +# Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline. +# В отличие от ConvertTo-Json (PS5.1): +# - не выравнивает ключи объекта по самому длинному +# - не разворачивает массивы примитивов на отдельные строки +# - кириллица в UTF-8 (без \uXXXX-escapes) +def convert_string_to_json_literal(s): + if s is None: + return 'null' + out = ['"'] + for ch in s: + code = ord(ch) + if code == 0x22: + out.append('\\"') + elif code == 0x5C: + out.append('\\\\') + elif code == 0x08: + out.append('\\b') + elif code == 0x09: + out.append('\\t') + elif code == 0x0A: + out.append('\\n') + elif code == 0x0C: + out.append('\\f') + elif code == 0x0D: + out.append('\\r') + elif code < 0x20: + out.append('\\u%04x' % code) + else: + out.append(ch) + out.append('"') + return ''.join(out) + + +def _fmt_number(v): + if isinstance(v, bool): + return 'true' if v else 'false' + if isinstance(v, int): + return str(v) + if isinstance(v, float): + # Invariant culture: '.' decimal sep + if v == int(v): + # Preserve float-ness: PS [double] 5.0 → "5" + # Match PS ToString invariant: 5.0 → "5" + return str(int(v)) + return repr(v) + return str(v) + + +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)): + return _fmt_number(obj) + if isinstance(obj, dict): + if len(obj) == 0: + return '{}' + parts = [] + for k, v in obj.items(): + vs = try_inline_json(v) + if vs is None: + return None + parts.append(convert_string_to_json_literal(str(k)) + ': ' + vs) + return '{ ' + ', '.join(parts) + ' }' + if isinstance(obj, (list, tuple)): + if len(obj) == 0: + return '[]' + parts = [] + for it in obj: + vs = try_inline_json(it) + if vs is None: + return None + parts.append(vs) + return '[' + ', '.join(parts) + ']' + return None + + +def convert_to_compact_json(obj, depth=0, indent_unit=' ', line_limit=400): + 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)): + return _fmt_number(obj) + + # Try inline для объектов и массивов с объектами — если помещается в lineLimit с учётом текущего indent. + 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): + if len(obj) == 0: + return '{}' + parts = [] + for k, v in obj.items(): + val = convert_to_compact_json(v, depth + 1, indent_unit, line_limit) + parts.append(child_indent + convert_string_to_json_literal(str(k)) + ': ' + val) + return "{\n" + ",\n".join(parts) + "\n" + indent + "}" + if isinstance(obj, (list, tuple)): + if len(obj) == 0: + return '[]' + parts = [child_indent + convert_to_compact_json(it, depth + 1, indent_unit, line_limit) for it in obj] + return "[\n" + ",\n".join(parts) + "\n" + indent + "]" + return convert_string_to_json_literal(str(obj)) + + +def get_text(node, xpath): + if not node: + return None + n = node.select_single_node(xpath) + if n: + return n.inner_text + return None + + +# Extract LocalStringType (multilingual title) → string (if only ru) or hashtable +def get_ml_text(node): + if not node: + return None + items = node.select_nodes("v8:item") + if len(items) == 0: + return None + d = {} + for it in items: + lang = get_text(it, "v8:lang") + content = get_text(it, "v8:content") + if lang: + d[lang] = content if content else "" + if len(d) == 1 and 'ru' in d: + return d['ru'] + return d + + +# Strip namespace prefix from xsi:type value (e.g. "dcsset:Foo" → "Foo") +def get_local_xsi_type(node): + if not node: + return None + t = node.get_attribute("type", NS_XSI) + if t and ':' in t: + return t.split(':', 1)[1] + return t if t else None + + +# Convert one element + sibling qualifiers → shorthand type string +def get_one_type_shorthand(type_node, qual_number, qual_string, qual_date): + raw = type_node.inner_text.strip() + # Strip namespace prefix; check if it's d5p1: (config refs) + local = raw + m = re.match(r'^([^:]+):(.+)$', raw) + if m: + prefix = m.group(1) + local = m.group(2) + uri = type_node.get_namespace_of_prefix(prefix) + if uri == NS_CFG: + return local + if uri == NS_XS: + if local == 'string': + if qual_string: + length = int(get_text(qual_string, "v8:Length") or 0) + allowed = get_text(qual_string, "v8:AllowedLength") + if length == 0: + return 'string' + if allowed == 'Fixed': + return "string(%d,fix)" % length + return "string(%d)" % length + return 'string' + if local == 'boolean': + return 'boolean' + if local == 'decimal': + if qual_number: + d = int(get_text(qual_number, "v8:Digits") or 0) + f = int(get_text(qual_number, "v8:FractionDigits") or 0) + sign = get_text(qual_number, "v8:AllowedSign") + sign_suf = '' + if sign == 'Nonnegative': + sign_suf = ',nonneg' + if f == 0: + return "decimal(%d%s)" % (d, sign_suf) + if sign_suf: + return "decimal(%d,%d%s)" % (d, f, sign_suf) + return "decimal(%d,%d)" % (d, f) + return 'decimal' + if local == 'dateTime': + frac = get_text(qual_date, "v8:DateFractions") if qual_date else 'DateTime' + if frac == 'Date': + return 'date' + if frac == 'Time': + return 'time' + return 'dateTime' + return local + if uri == NS_V8: + return local + return local + + +# valueType → string shorthand OR array of shorthands (composite) +def get_value_type_shorthand(value_type_node): + if not value_type_node: + return None + types = value_type_node.select_nodes("v8:Type") + type_sets = value_type_node.select_nodes("v8:TypeSet") + if len(types) == 0 and len(type_sets) == 0: + return None + qual_n = value_type_node.select_single_node("v8:NumberQualifiers") + qual_s = value_type_node.select_single_node("v8:StringQualifiers") + qual_d = value_type_node.select_single_node("v8:DateQualifiers") + shorts = [] + for t in types: + shorts.append(get_one_type_shorthand(t, qual_n, qual_s, qual_d)) + for ts in type_sets: + txt = ts.inner_text + m = re.match(r'.*:(.+)$', txt) + if m: + shorts.append(m.group(1)) + else: + shorts.append(txt) + if len(shorts) == 1: + return shorts[0] + return shorts + + +# → { tokens, extras } +def get_role_info(role_node, loc): + if not role_node: + return None + tokens = [] + extras = {} + has_complex = False + pn_node = role_node.select_single_node("dcscom:periodNumber") + pt_node = role_node.select_single_node("dcscom:periodType") + period_handled = False + if pn_node and pt_node and pn_node.inner_text == '1' and pt_node.inner_text == 'Main': + tokens.append('@period') + period_handled = True + for child in role_node.child_nodes: + if child.namespace_uri != NS_COM: + has_complex = True + continue + if period_handled and (child.local_name == 'periodNumber' or child.local_name == 'periodType'): + continue + txt = child.inner_text + if txt == 'true': + tokens.append('@' + child.local_name) + elif txt == 'false' or not txt: + pass + else: + extras[child.local_name] = txt + if has_complex: + new_sentinel('ComplexRole', loc, 'Роль с не-dcscom-атрибутами не сворачивается в DSL') + return {'tokens': tokens, 'extras': extras} + + +def render_role(tokens, extras): + has_extras = extras and len(extras) > 0 + has_tokens = tokens and len(tokens) > 0 + if not has_extras and not has_tokens: + return None + if not has_extras: + plain = [re.sub(r'^@', '', t) for t in tokens] + if len(plain) == 1: + return {'value': plain[0], 'isString': True} + with_at = ['@' + p for p in plain] + return {'value': ' '.join(with_at), 'isString': True} + all_simple = True + for v in extras.values(): + if not re.match(r'^[\w\.\-]+$', str(v)): + all_simple = False + break + if all_simple: + parts = [] + for t in tokens: + parts.append(t) + for k, v in extras.items(): + parts.append("%s=%s" % (k, v)) + return {'value': ' '.join(parts), 'isString': True} + obj = {} + for t in tokens: + obj[re.sub(r'^@', '', t)] = True + for k, v in extras.items(): + obj[k] = v + return {'value': obj, 'isString': False} + + +# → array of #tokens +def get_restriction_tokens(ur_node): + if not ur_node: + return [] + tokens = [] + m = {'field': '#noField', 'condition': '#noFilter', 'group': '#noGroup', 'order': '#noOrder'} + for key in ('field', 'condition', 'group', 'order'): + v = get_text(ur_node, "r:" + key) + if v == 'true': + tokens.append(m[key]) + return tokens + + +# → hashtable {param: value} +def get_font_value(val_node): + f = {'@type': 'Font'} + for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + a = val_node.el.get(attr_name) + if a is not None: + f[attr_name] = a + return f + + +def get_appearance_dict(app_node): + if not app_node: + return None + d = {} + items = app_node.select_nodes("dcscor:item") + for it in items: + p = get_text(it, "dcscor:parameter") + val_node = it.select_single_node("dcscor:value") + if not p or not val_node: + continue + val_type = get_local_xsi_type(val_node) + if val_type == 'LocalStringType': + raw_val = get_ml_text(val_node) + elif val_type == 'Font': + raw_val = get_font_value(val_node) + else: + raw_val = val_node.inner_text + use_v = get_text(it, "dcscor:use") + if use_v == 'false': + d[p] = {'value': raw_val, 'use': False} + else: + d[p] = raw_val + return d + + +# Read → JSON array. Returns None если отсутствует или пустой. +def read_input_parameters(parent_node): + ip = parent_node.select_single_node("r:inputParameters") + if not ip: + return None + result = [] + for it in ip.select_nodes("dcscor:item"): + entry = {} + use_text = get_text(it, "dcscor:use") + p_name = get_text(it, "dcscor:parameter") + entry['parameter'] = p_name + if use_text == 'false': + entry['use'] = False + val = it.select_single_node("dcscor:value") + if val: + v_type = get_local_xsi_type(val) + if v_type == 'ChoiceParameters': + cp = [] + for cp_item in val.select_nodes("dcscor:item"): + cp_entry = {'name': get_text(cp_item, "dcscor:choiceParameter")} + values = [] + for v in cp_item.select_nodes("dcscor:value"): + v_xsi = get_local_xsi_type(v) + v_txt = v.inner_text + if v_xsi == 'boolean': + values.append(v_txt == 'true') + elif v_xsi == 'decimal': + if re.match(r'^-?\d+$', v_txt or ''): + values.append(int(v_txt)) + else: + values.append(float(v_txt)) + else: + values.append(v_txt) + cp_entry['values'] = values + cp.append(cp_entry) + entry['choiceParameters'] = cp + elif v_type == 'ChoiceParameterLinks': + cpl = [] + for cpl_item in val.select_nodes("dcscor:item"): + cpl_entry = { + 'name': get_text(cpl_item, "dcscor:choiceParameter"), + 'value': get_text(cpl_item, "dcscor:value"), + } + mode = get_text(cpl_item, "dcscor:mode") + if mode: + cpl_entry['mode'] = mode + cpl.append(cpl_entry) + entry['choiceParameterLinks'] = cpl + elif v_type == 'LocalStringType': + ml = get_ml_text(val) + entry['value'] = ml if ml else '' + else: + txt = val.inner_text + if v_type == 'boolean': + entry['value'] = (txt == 'true') + elif v_type == 'decimal': + if re.match(r'^-?\d+$', txt or ''): + entry['value'] = int(txt) + else: + entry['value'] = float(txt) + else: + entry['value'] = txt + ta = val.get_attr_raw('xsi:type') + if ta and not re.match(r'^xs:', ta): + prefix = ta.split(':', 1)[0] + local_name = ta.split(':', 1)[1] + uri = val.get_namespace_of_prefix(prefix) + if uri: + entry['valueType'] = {'uri': uri, 'name': local_name} + result.append(entry) + if len(result) == 0: + return None + return result + + +# Build a field JSON entry (shorthand if possible, object form otherwise) +def build_field(field_node, loc): + input_parameters = read_input_parameters(field_node) + order_expr_nodes = field_node.select_nodes("r:orderExpression") + order_expression = None + order_expression_list = [] + for oe_n in order_expr_nodes: + oe_expr = get_text(oe_n, "dcscom:expression") + oe_type = get_text(oe_n, "dcscom:orderType") + oe_auto = get_text(oe_n, "dcscom:autoOrder") + oe = {} + if oe_expr: + oe['expression'] = oe_expr + if oe_type: + oe['orderType'] = oe_type + if oe_auto == 'true': + oe['autoOrder'] = True + elif oe_auto == 'false': + oe['autoOrder'] = False + order_expression_list.append(oe) + if len(order_expression_list) == 1: + order_expression = order_expression_list[0] + elif len(order_expression_list) > 1: + order_expression = order_expression_list + data_path = get_text(field_node, "r:dataPath") + field_name = get_text(field_node, "r:field") + title_node = field_node.select_single_node("r:title") + title = get_ml_text(title_node) + value_type_node = field_node.select_single_node("r:valueType") + type_short = get_value_type_shorthand(value_type_node) + role_info = get_role_info(field_node.select_single_node("r:role"), "%s/role" % loc) + role_tokens = role_info['tokens'] if role_info else [] + role_extras = role_info['extras'] if role_info else {} + role_rendered = render_role(role_tokens, role_extras) + restrict_tokens = get_restriction_tokens(field_node.select_single_node("r:useRestriction")) + attr_restrict_tokens = get_restriction_tokens(field_node.select_single_node("r:attributeUseRestriction")) + app_node = field_node.select_single_node("r:appearance") + appearance = get_appearance_dict(app_node) + pres_expr = get_text(field_node, "r:presentationExpression") + av_nodes = field_node.select_nodes("r:availableValue") + available_values = [] + for av in av_nodes: + av_vn = av.select_single_node("r:value") + av_pn = av.select_single_node("r:presentation") + av_entry = {} + if av_vn: + av_type = get_local_xsi_type(av_vn) + av_text = av_vn.inner_text + if av_type == 'boolean': + av_entry['value'] = (av_text == 'true') + elif av_type == 'decimal': + if re.match(r'^-?\d+$', av_text or ''): + av_entry['value'] = int(av_text) + else: + av_entry['value'] = float(av_text) + else: + av_entry['value'] = av_text + if av_pn: + av_pres = get_ml_text(av_pn) + if av_pres: + av_entry['presentation'] = av_pres + available_values.append(av_entry) + + role_in_string = role_rendered and role_rendered['isString'] + needs_object = bool(title) or bool(appearance) or bool(pres_expr) or isinstance(type_short, list) \ + or (role_rendered and not role_in_string) or bool(order_expression) or bool(input_parameters) \ + or (len(available_values) > 0) or (attr_restrict_tokens and len(attr_restrict_tokens) > 0) + + if not needs_object: + s = field_name + if type_short: + s = "%s: %s" % (field_name, type_short) + if role_in_string: + rv = role_rendered['value'] + if '@' in rv or '=' in rv or re.search(r'\s', rv): + s += ' ' + rv + else: + s += " @%s" % rv + if restrict_tokens: + s += ' ' + ' '.join(restrict_tokens) + if not (data_path and data_path != field_name): + return s + + obj = {'field': field_name} + if data_path and data_path != field_name: + obj['dataPath'] = data_path + if title: + obj['title'] = title + if type_short: + obj['type'] = type_short + if role_rendered: + obj['role'] = role_rendered['value'] + if order_expression: + obj['orderExpression'] = order_expression + if input_parameters: + obj['inputParameters'] = input_parameters + if restrict_tokens: + obj['restrict'] = [re.sub(r'^#', '', t) for t in restrict_tokens] + if attr_restrict_tokens and len(attr_restrict_tokens) > 0: + obj['attrRestrict'] = [re.sub(r'^#', '', t) for t in attr_restrict_tokens] + if pres_expr: + obj['presentationExpression'] = pres_expr + if len(available_values) > 0: + obj['availableValues'] = available_values + if appearance: + obj['appearance'] = appearance + return obj + + +# Build calculatedField → shorthand string or object form +def build_calc_field(cf_node, loc): + data_path = get_text(cf_node, "r:dataPath") + expression = get_text(cf_node, "r:expression") + title_node = cf_node.select_single_node("r:title") + title = get_ml_text(title_node) + value_type_node = cf_node.select_single_node("r:valueType") + type_short = get_value_type_shorthand(value_type_node) + restrict_tokens = get_restriction_tokens(cf_node.select_single_node("r:useRestriction")) + app_node = cf_node.select_single_node("r:appearance") + appearance = get_appearance_dict(app_node) + + title_needs_object = isinstance(title, dict) or isinstance(type_short, list) + needs_object = bool(appearance) or title_needs_object + + if not needs_object: + s = data_path + if title: + s += " [%s]" % title + if type_short: + s += ": %s" % type_short + if expression: + s += " = %s" % expression + if restrict_tokens: + s += ' ' + ' '.join(restrict_tokens) + return s + + obj = {'name': data_path} + if title: + obj['title'] = title + if type_short: + obj['type'] = type_short + if expression: + obj['expression'] = expression + if restrict_tokens: + obj['restrict'] = [re.sub(r'^#', '', t) for t in restrict_tokens] + if appearance: + obj['appearance'] = appearance + return obj + + +# Build totalField → shorthand or object form +def build_total_field(tf_node): + data_path = get_text(tf_node, "r:dataPath") + expression = get_text(tf_node, "r:expression") or '' + group_nodes = tf_node.select_nodes("r:group") + has_groups = bool(group_nodes) and len(group_nodes) > 0 + + if has_groups or re.search(r'[\r\n]', expression): + obj = {'dataPath': data_path, 'expression': expression} + if has_groups: + groups = [] + for g in group_nodes: + groups.append(g.inner_text) + obj['group'] = groups + return obj + + m = re.match(r'^(\w+)\((\w+)\)$', expression) + if m and m.group(2) == data_path: + return "%s: %s" % (data_path, m.group(1)) + return "%s: %s" % (data_path, expression) + + +# Detect StandardPeriod variant from node +def get_standard_period_variant(value_node): + if not value_node: + return None + variant = get_text(value_node, "v8:variant") + if variant: + return variant + return None + + +# Build parameter → shorthand or object form +def build_parameter(p_node, loc): + name = get_text(p_node, "r:name") + title_node = p_node.select_single_node("r:title") + title = get_ml_text(title_node) + value_type_node = p_node.select_single_node("r:valueType") + type_short = get_value_type_shorthand(value_type_node) + + value_nodes = p_node.select_nodes("r:value") + value_display = None + value_is_nil = False + if len(value_nodes) > 1: + value_arr = [] + for vn in value_nodes: + vt = get_local_xsi_type(vn) + v_txt = vn.inner_text + if vt == 'boolean': + value_arr.append(v_txt == 'true') + elif vt == 'decimal': + if re.match(r'^-?\d+$', v_txt or ''): + value_arr.append(int(v_txt)) + else: + value_arr.append(float(v_txt)) + else: + value_arr.append(v_txt) + value_display = value_arr + elif len(value_nodes) == 1: + value_node = value_nodes[0] + nil = value_node.get_attribute("nil", NS_XSI) + if nil == 'true': + value_is_nil = True + else: + v_type = get_local_xsi_type(value_node) + if v_type == 'StandardPeriod': + variant = get_text(value_node, "v8:variant") + sd = get_text(value_node, "v8:startDate") + ed = get_text(value_node, "v8:endDate") + has_explicit_dates = (sd and sd != '0001-01-01T00:00:00') or (ed and ed != '0001-01-01T00:00:00') + if has_explicit_dates: + value_display = {'variant': variant} + if sd: + value_display['startDate'] = sd + if ed: + value_display['endDate'] = ed + elif variant and variant != 'Custom': + value_display = variant + elif v_type == 'DesignTimeValue': + value_display = value_node.inner_text + elif v_type == 'LocalStringType': + value_display = get_ml_text(value_node) + else: + txt = value_node.inner_text + if txt: + value_display = txt + + value_list_allowed = (get_text(p_node, "r:valueListAllowed") == 'true') + available_as_field = get_text(p_node, "r:availableAsField") + deny_incomplete = (get_text(p_node, "r:denyIncompleteValues") == 'true') + use_attr = get_text(p_node, "r:use") + use_restriction = (get_text(p_node, "r:useRestriction") == 'true') + expression = get_text(p_node, "r:expression") + input_parameters = read_input_parameters(p_node) + not_a_field = (available_as_field == 'false') + hidden = not_a_field and use_restriction + + av_nodes = p_node.select_nodes("r:availableValue") + available_values = [] + for av in av_nodes: + av_val_node = av.select_single_node("r:value") + av_pres_node = av.select_single_node("r:presentation") + av_entry = {} + if av_val_node: + av_type = get_local_xsi_type(av_val_node) + av_text = av_val_node.inner_text + if av_type == 'boolean': + av_entry['value'] = (av_text == 'true') + elif av_type == 'decimal': + if re.match(r'^-?\d+$', av_text or ''): + av_entry['value'] = int(av_text) + else: + av_entry['value'] = float(av_text) + else: + av_entry['value'] = av_text + if av_pres_node: + av_entry['presentation'] = get_ml_text(av_pres_node) + available_values.append(av_entry) + + result = { + 'name': name, + 'title': title, + 'typeShort': type_short, + 'valueDisplay': value_display, + 'valueIsNil': value_is_nil, + 'valueListAllowed': value_list_allowed, + 'hidden': hidden, + 'notAField': (not_a_field and not hidden), + 'denyIncomplete': deny_incomplete, + 'useAttr': use_attr, + 'useRestriction': use_restriction, + 'expression': expression, + 'availableValues': available_values, + 'inputParameters': input_parameters, + } + return result + + +# Render parameter (after autoDates folding) → shorthand or object form +def render_parameter(p): + name = p['name'] + title = p['title'] + type_short = p['typeShort'] + value_display = p['valueDisplay'] + value_is_nil = p['valueIsNil'] + flags = [] + if p.get('autoDates'): + flags.append('@autoDates') + if p['valueListAllowed']: + flags.append('@valueList') + if p['hidden']: + flags.append('@hidden') + + title_needs_object = isinstance(title, dict) + type_is_array = isinstance(type_short, list) + value_is_dict = isinstance(value_display, dict) + + needs_object = False + if p['availableValues'] and len(p['availableValues']) > 0: + needs_object = True + if p['inputParameters']: + needs_object = True + if title_needs_object: + needs_object = True + if type_is_array: + needs_object = True + if value_is_dict: + needs_object = True + if not p.get('autoDates'): + if p['denyIncomplete']: + needs_object = True + if p['useAttr']: + needs_object = True + if p['useRestriction'] and not p['hidden'] and not p.get('autoDates'): + needs_object = True + if p['expression']: + needs_object = True + if p['notAField']: + needs_object = True + + ref_type_pattern = r'^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan|CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|InformationRegisterRef|ExchangePlanRef|AnyRef)' + type_is_ref = False + if isinstance(type_short, str) and re.match(ref_type_pattern, type_short): + type_is_ref = True + nil_needs_object = value_is_nil and not type_is_ref and type_short and not isinstance(type_short, list) + if nil_needs_object: + needs_object = True + + if not needs_object: + s = name + if title: + s += " [%s]" % title + if type_short: + s += ": %s" % type_short + if not value_is_nil and value_display is not None and value_display != '': + s += " = %s" % value_display + if flags: + s += ' ' + ' '.join(flags) + return s + + obj = {'name': name} + if title: + obj['title'] = title + if type_short: + obj['type'] = type_short + if not value_is_nil and value_display is not None and value_display != '': + obj['value'] = value_display + if nil_needs_object: + obj['nilValue'] = True + if p['useAttr'] and not p.get('autoDates'): + obj['use'] = p['useAttr'] + if p['denyIncomplete'] and not p.get('autoDates'): + obj['denyIncompleteValues'] = True + if p['hidden']: + obj['hidden'] = True + if p['notAField']: + obj['availableAsField'] = False + if p['valueListAllowed']: + obj['valueListAllowed'] = True + if p.get('autoDates'): + obj['autoDates'] = True + if p['expression']: + obj['expression'] = p['expression'] + if p['useRestriction'] and not p['hidden'] and not p.get('autoDates'): + obj['useRestriction'] = True + if p['availableValues'] and len(p['availableValues']) > 0: + obj['availableValues'] = p['availableValues'] + if p['inputParameters']: + obj['inputParameters'] = p['inputParameters'] + return obj + + +# --- 3b. Built-in style presets (preset-shape: 11 полей) --- + +builtin_preset_names = ['none', 'data', 'header', 'subheader', 'total'] + +builtin_presets = { + 'none': { + 'font': None, 'fontSize': None, 'bold': False, 'italic': False, + 'hAlign': None, 'vAlign': None, 'wrap': False, + 'bgColor': None, 'textColor': None, + 'borderColor': None, 'borders': False, + }, + 'data': { + 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False, + 'hAlign': None, 'vAlign': None, 'wrap': False, + 'bgColor': 'style:ReportGroup1BackColor', 'textColor': None, + 'borderColor': 'style:ReportLineColor', 'borders': True, + }, + 'header': { + 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False, + 'hAlign': 'Center', 'vAlign': None, 'wrap': True, + 'bgColor': 'style:ReportHeaderBackColor', 'textColor': None, + 'borderColor': 'style:ReportLineColor', 'borders': True, + }, + 'subheader': { + 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False, + 'hAlign': 'Center', 'vAlign': None, 'wrap': True, + 'bgColor': None, 'textColor': None, + 'borderColor': 'style:ReportLineColor', 'borders': True, + }, + 'total': { + 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False, + 'hAlign': None, 'vAlign': None, 'wrap': False, + 'bgColor': None, 'textColor': None, + 'borderColor': 'style:ReportLineColor', 'borders': True, + }, +} + +effective_presets = {} +for k in builtin_presets: + copy = {} + for f in builtin_presets[k]: + copy[f] = builtin_presets[k][f] + effective_presets[k] = copy + +existing_user_presets_raw = None + +query_files_accumulator = [] +query_file_names_used = {} + +output_dir = None +output_basename = None + + +def maybe_externalize_query(query_text, dataset_name): + if not query_text: + return query_text + if not output_dir: + return query_text + line_count = len(re.findall(r'\n', query_text)) + 1 + if line_count < 3: + return query_text + safe_ds = re.sub(r'[^\w\-]', '_', dataset_name) + if not safe_ds: + safe_ds = 'query' + prefix = "%s-" % output_basename if output_basename else '' + file_name = "%s%s.sql" % (prefix, safe_ds) + suffix = 1 + while file_name in query_file_names_used: + suffix += 1 + file_name = "%s%s_%d.sql" % (prefix, safe_ds, suffix) + query_file_names_used[file_name] = True + query_files_accumulator.append({'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: + path = os.path.join(output_dir, qf['fileName']) + with open(path, 'w', encoding='utf-8', newline='') as f: + f.write(qf['text']) + print("Saved %d external query file(s)" % len(query_files_accumulator), file=sys.stderr) + + +custom_styles_accumulator = {} +custom_style_counter = [0] + + +def normalize_color(val_node): + if not val_node: + return None + txt = val_node.inner_text + m = re.match(r'^([^:]+):(.+)$', txt or '') + if m: + pfx = m.group(1) + name = m.group(2) + uri = val_node.get_namespace_of_prefix(pfx) + if uri == 'http://v8.1c.ru/8.1/data/ui/style': + return 'style:' + name + if uri == 'http://v8.1c.ru/8.1/data/ui/colors/web': + return 'web:' + name + if uri == 'http://v8.1c.ru/8.1/data/ui/colors/windows': + return 'win:' + name + return txt + + +def extract_cell_preset(app_node): + if not app_node: + return None + preset = { + 'font': None, 'fontSize': None, 'bold': False, 'italic': False, + 'hAlign': None, 'vAlign': None, 'wrap': False, + 'bgColor': None, 'textColor': None, + 'borderColor': None, 'borders': False, + } + has_any_style = False + for it in app_node.select_nodes("dcscor:item"): + p_name = get_text(it, "dcscor:parameter") + val = it.select_single_node("dcscor:value") + if not p_name: + continue + if p_name in ('МинимальнаяШирина', 'МаксимальнаяШирина', 'МинимальнаяВысота', 'ОбъединятьПоВертикали', 'ОбъединятьПоГоризонтали', 'Расшифровка'): + continue + if p_name == 'Шрифт': + if val: + preset['font'] = val.el.get('faceName') + h = val.el.get('height') + if h: + preset['fontSize'] = int(h) + preset['bold'] = (val.el.get('bold') == 'true') + preset['italic'] = (val.el.get('italic') == 'true') + has_any_style = True + elif p_name == 'ЦветФона': + if val: + preset['bgColor'] = normalize_color(val) + has_any_style = True + elif p_name == 'ЦветТекста': + if val: + preset['textColor'] = normalize_color(val) + has_any_style = True + elif p_name == 'ЦветГраницы': + if val: + preset['borderColor'] = normalize_color(val) + has_any_style = True + elif p_name == 'СтильГраницы': + sides_found = 0 + for sub in it.select_nodes("dcscor:item"): + sub_name = get_text(sub, "dcscor:parameter") + if sub_name and re.match(r'^СтильГраницы\.(Слева|Сверху|Справа|Снизу)$', sub_name): + sides_found += 1 + if sides_found > 0: + preset['borders'] = True + has_any_style = True + elif p_name == 'ГоризонтальноеПоложение': + if val: + preset['hAlign'] = val.inner_text + has_any_style = True + elif p_name == 'ВертикальноеПоложение': + if val: + preset['vAlign'] = val.inner_text + has_any_style = True + elif p_name == 'Размещение': + if val and val.inner_text == 'Wrap': + preset['wrap'] = True + has_any_style = True + if not has_any_style: + return None + return preset + + +def compare_preset(a, b): + for key in ('font', 'fontSize', 'bold', 'italic', 'hAlign', 'vAlign', 'wrap', 'bgColor', 'textColor', 'borderColor', 'borders'): + if a[key] != b[key]: + return False + return True + + +def match_preset_by_shape(cell_preset): + if not cell_preset: + return None + for name in effective_presets: + if compare_preset(cell_preset, effective_presets[name]): + return name + return None + + +def allocate_custom_style(cell_preset): + custom_style_counter[0] += 1 + name = "custom%d" % custom_style_counter[0] + while name in effective_presets: + custom_style_counter[0] += 1 + name = "custom%d" % custom_style_counter[0] + effective_presets[name] = cell_preset + custom_styles_accumulator[name] = cell_preset + return name + + +def load_user_styles(dir_path): + global existing_user_presets_raw + if not dir_path: + return + styles_path = os.path.join(dir_path, 'skd-styles.json') + if not os.path.exists(styles_path): + return + import json as _json + with open(styles_path, 'r', encoding='utf-8-sig') as f: + raw = _json.load(f) + existing_user_presets_raw = raw + for prop_name, prop_value in raw.items(): + preset = {} + for k in builtin_presets['data']: + preset[k] = builtin_presets['data'][k] + if prop_name in builtin_presets: + for k in builtin_presets[prop_name]: + preset[k] = builtin_presets[prop_name][k] + for up_name, up_value in prop_value.items(): + preset[up_name] = up_value + effective_presets[prop_name] = preset + + +def save_user_styles(dir_path): + if not dir_path: + return + if len(custom_styles_accumulator) == 0 and not existing_user_presets_raw: + return + styles_path = os.path.join(dir_path, 'skd-styles.json') + out = {} + if existing_user_presets_raw: + for prop_name, prop_value in existing_user_presets_raw.items(): + out[prop_name] = prop_value + for name in custom_styles_accumulator: + if name in out: + continue + out[name] = custom_styles_accumulator[name] + if len(out) == 0: + return + json_text = convert_to_compact_json(out) + with open(styles_path, 'w', encoding='utf-8', newline='') as f: + f.write(json_text) + print("Saved skd-styles.json (custom styles: %d)" % len(custom_styles_accumulator), file=sys.stderr) + + +def get_cell_per_cell_attrs(app_node): + attrs = {'width': None, 'height': None, 'mergeV': False, 'mergeH': False, 'drilldown': None, 'drilldownTarget': None} + if not app_node: + return attrs + for it in app_node.select_nodes("dcscor:item"): + p_name = get_text(it, "dcscor:parameter") + val = it.select_single_node("dcscor:value") + if not p_name: + continue + if p_name == 'МинимальнаяШирина': + if val: + attrs['width'] = val.inner_text + elif p_name == 'МинимальнаяВысота': + if val: + attrs['height'] = val.inner_text + elif p_name == 'ОбъединятьПоВертикали': + if val and val.inner_text == 'true': + attrs['mergeV'] = True + elif p_name == 'ОбъединятьПоГоризонтали': + if val and val.inner_text == 'true': + attrs['mergeH'] = True + elif p_name == 'Расшифровка': + if val: + param_ref = val.inner_text + attrs['drilldownTarget'] = param_ref + m = re.match(r'^Расшифровка_(.+)$', param_ref or '') + if m: + attrs['drilldown'] = m.group(1) + return attrs + + +def get_cell_content(cell_node, per_cell_attrs): + if per_cell_attrs['mergeV']: + return '|' + if per_cell_attrs['mergeH']: + return '>' + + item = cell_node.select_single_node("dcsat:item") + if not item: + return None + item_type = get_local_xsi_type(item) + val_node = item.select_single_node("dcsat:value") + if not val_node: + return None + val_type = get_local_xsi_type(val_node) + + if item_type == 'Field' and val_type == 'Parameter': + return '{' + val_node.inner_text + '}' + if val_type == 'LocalStringType': + text = get_ml_text(val_node) + if isinstance(text, dict): + return text + return text + return val_node.inner_text + + +def build_template_parameter(p_node): + p_type = get_local_xsi_type(p_node) + obj = {} + obj['name'] = get_text(p_node, "dcsat:name") + if p_type == 'ExpressionAreaTemplateParameter': + obj['expression'] = get_text(p_node, "dcsat:expression") + elif p_type == 'DetailsAreaTemplateParameter': + obj['__details__'] = True + obj['expression'] = get_text(p_node, "dcsat:expression") + return obj + + +def build_template(template_node, loc): + tmpl_obj = {'name': get_text(template_node, "r:name")} + inner = template_node.select_single_node("r:template") + if not inner: + return tmpl_obj + + row_nodes = inner.select_nodes("dcsat:item[@xsi:type='dcsat:TableRow']") + if len(row_nodes) == 0: + all_items = inner.select_nodes("dcsat:item") + row_nodes = [] + for n in all_items: + if get_local_xsi_type(n) == 'TableRow': + row_nodes.append(n) + + rows = [] + widths = None + min_height = None + cell_style_map = {} + cell_drilldown_map = {} + has_any_styled_cell = False + drilldown_by_param = {} + + row_idx = 0 + for row_node in row_nodes: + cells = [] + cell_nodes = row_node.select_nodes("dcsat:tableCell") + col_idx = 0 + row_widths = [] + for cell_node in cell_nodes: + app_node = cell_node.select_single_node("dcsat:appearance") + per_cell = get_cell_per_cell_attrs(app_node) + content = get_cell_content(cell_node, per_cell) + + if app_node and not per_cell['mergeV'] and not per_cell['mergeH']: + cell_preset = extract_cell_preset(app_node) + if cell_preset is not None: + matched = match_preset_by_shape(cell_preset) + if matched is None: + matched = allocate_custom_style(cell_preset) + cell_style_map["%d,%d" % (row_idx, col_idx)] = matched + has_any_styled_cell = True + + m_content = re.match(r'^\{(.+)\}$', content) if isinstance(content, str) else None + if m_content and per_cell['drilldown']: + drilldown_by_param[m_content.group(1)] = per_cell['drilldown'] + if per_cell['drilldownTarget']: + cell_drilldown_map["%d,%d" % (row_idx, col_idx)] = per_cell['drilldownTarget'] + + if row_idx == 0 and per_cell['width']: + row_widths.append(per_cell['width']) + if row_idx == 0 and col_idx == 0 and per_cell['height']: + min_height = per_cell['height'] + + cells.append(content) + col_idx += 1 + if row_idx == 0 and len(row_widths) > 0: + widths = row_widths + rows.append(cells) + row_idx += 1 + + template_default = None + if has_any_styled_cell: + counts = {} + for k in cell_style_map: + name = cell_style_map[k] + if name not in counts: + counts[name] = 0 + counts[name] += 1 + max_count = 0 + for name in counts: + if counts[name] > max_count: + max_count = counts[name] + template_default = name + + if template_default: + rows_out = [] + for r in range(len(rows)): + new_row = [] + for c in range(len(rows[r])): + key = "%d,%d" % (r, c) + if key in cell_style_map and cell_style_map[key] != template_default: + new_row.append({'value': rows[r][c], 'style': cell_style_map[key]}) + else: + new_row.append(rows[r][c]) + rows_out.append(new_row) + rows = rows_out + + param_nodes = template_node.select_nodes("r:parameter") + expr_params = {} + details_by_name = {} + for pn in param_nodes: + p_type = get_local_xsi_type(pn) + p_name = get_text(pn, "dcsat:name") + if p_type == 'ExpressionAreaTemplateParameter': + expr_params[p_name] = get_text(pn, "dcsat:expression") + elif p_type == 'DetailsAreaTemplateParameter': + fe_node = pn.select_single_node("dcsat:fieldExpression") + details_by_name[p_name] = { + 'field': get_text(fe_node, "dcsat:field") if fe_node else '', + 'expression': get_text(fe_node, "dcsat:expression") if fe_node else '', + 'action': get_text(pn, "dcsat:mainAction"), + } + + folded_detail_names = {} + for pname in list(drilldown_by_param.keys()): + y_val = drilldown_by_param[pname] + detail_name = "Расшифровка_%s" % y_val + if detail_name in details_by_name: + d = details_by_name[detail_name] + expected_expr = '"%s"' % y_val + if d['field'] == 'ИмяРесурса' and d['expression'] == expected_expr and d['action'] == 'DrillDown': + folded_detail_names[detail_name] = True + + template_params = [] + for pname in expr_params: + entry = {'name': pname, 'expression': expr_params[pname]} + if pname in drilldown_by_param: + entry['drilldown'] = drilldown_by_param[pname] + template_params.append(entry) + for dname in details_by_name: + if dname in folded_detail_names: + continue + d = details_by_name[dname] + entry = {'name': dname} + dd_obj = {'field': d['field'], 'expression': d['expression']} + if d['action'] and d['action'] != 'DrillDown': + dd_obj['action'] = d['action'] + entry['drilldown'] = dd_obj + template_params.append(entry) + + if len(cell_drilldown_map) > 0: + for r in range(len(rows)): + new_row = [] + for c in range(len(rows[r])): + cell_val = rows[r][c] + key = "%d,%d" % (r, c) + target = cell_drilldown_map.get(key) + inner_val = cell_val + is_wrapped = False + if isinstance(cell_val, dict): + if 'value' in cell_val: + inner_val = cell_val['value'] + is_wrapped = True + needs_wrap = False + if target and isinstance(inner_val, str): + m = re.match(r'^\{(.+)\}$', inner_val) + if m: + cell_param = m.group(1) + expected_shortcut = None + if cell_param in drilldown_by_param: + expected_shortcut = "Расшифровка_%s" % drilldown_by_param[cell_param] + if target != expected_shortcut: + needs_wrap = True + if needs_wrap: + if is_wrapped: + cell_val['drilldown'] = target + new_row.append(cell_val) + else: + new_row.append({'value': inner_val, 'drilldown': target}) + else: + new_row.append(cell_val) + rows[r] = new_row + + if template_default: + tmpl_obj['style'] = template_default + elif len(rows) > 0: + tmpl_obj['style'] = 'none' + if widths: + tmpl_obj['widths'] = widths + if min_height: + tmpl_obj['minHeight'] = min_height + tmpl_obj['rows'] = rows + if len(template_params) > 0: + tmpl_obj['parameters'] = template_params + + return tmpl_obj + + +# --- 3c. Filter / settings helpers --- + +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', + 'Filled': 'filled', 'NotFilled': 'notFilled', +} + + +def get_filter_value(val_node): + if not val_node: + return '_' + nil = val_node.get_attribute("nil", NS_XSI) + if nil == 'true': + return '_' + v_type = get_local_xsi_type(val_node) + if v_type == 'DesignTimeValue': + return val_node.inner_text + if v_type == 'LocalStringType': + return get_ml_text(val_node) + txt = val_node.inner_text + if not txt: + return '_' + return txt + + +def get_filter_value_with_type(val_node): + if not val_node: + return {'value': '_', 'type': None} + raw_type = val_node.get_attribute("type", NS_XSI) + nil = val_node.get_attribute("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} + txt = val_node.inner_text + 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} + + +def build_filter_item(item_node, loc): + xtype = get_local_xsi_type(item_node) + if xtype == 'FilterItemGroup': + gt = get_text(item_node, "dcsset:groupType") + if gt == 'OrGroup': + group_name = 'Or' + elif gt == 'NotGroup': + group_name = 'Not' + else: + group_name = 'And' + items = [] + for c in item_node.select_nodes("dcsset:item"): + items.append(build_filter_item(c, "%s/item" % loc)) + g_obj = {'group': group_name, 'items': items} + g_pres_node = item_node.select_single_node("dcsset:presentation") + if g_pres_node: + g_pres = get_ml_text(g_pres_node) + if not g_pres: + g_pres = g_pres_node.inner_text + if g_pres: + g_obj['presentation'] = g_pres + g_vm_node = item_node.select_single_node("dcsset:viewMode") + if g_vm_node: + g_obj['viewMode'] = g_vm_node.inner_text + g_usid = get_text(item_node, "dcsset:userSettingID") + if g_usid: + g_obj['userSettingID'] = 'auto' + g_uspn = item_node.select_single_node("dcsset:userSettingPresentation") + if g_uspn: + g_usp = get_ml_text(g_uspn) + if g_usp: + g_obj['userSettingPresentation'] = g_usp + return g_obj + if xtype != 'FilterItemComparison': + return new_sentinel("FilterItemType:%s" % xtype, loc, 'Неизвестный тип фильтра') + left_node = item_node.select_single_node("dcsset:left") + field = left_node.inner_text if left_node else None + ct = get_text(item_node, "dcsset:comparisonType") + op = filter_op_map.get(ct) + if not op: + op = ct + + right_nodes = item_node.select_nodes("dcsset:right") + 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 \ + re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', 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 len(right_nodes) > 1: + arr = [] + raw_types = [] + for rn in right_nodes: + arr.append(get_filter_value(rn)) + raw_types.append(rn.get_attribute("type", NS_XSI)) + value = arr + value_is_array_flag = True + uniq_types = sorted(set(t for t in raw_types if t)) + # PS does Sort-Object -Unique, preserves None as well; we filtered. Replicate "all same": + uniq_all = sorted(set(raw_types)) + if len(uniq_all) == 1 and uniq_all[0]: + t0 = uniq_all[0] + if t0 == 'dcscor:DesignTimeValue' and len(arr) > 0: + all_match = all(re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(x)) for x in arr) + auto_detects_dtv = all_match + else: + auto_detects_dtv = False + if not auto_detects_dtv: + value_type_attr = t0 + + use = get_text(item_node, "dcsset:use") + user_id = get_text(item_node, "dcsset:userSettingID") + vm_node = item_node.select_single_node("dcsset:viewMode") + view_mode = vm_node.inner_text if vm_node else None + user_pres_node = item_node.select_single_node("dcsset:userSettingPresentation") + fi_pres_node = item_node.select_single_node("dcsset:presentation") + fi_pres = None + if fi_pres_node: + fi_pres = get_ml_text(fi_pres_node) + if not fi_pres: + fi_pres = fi_pres_node.inner_text + + 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') + + if user_pres_node or value_is_array_flag or value_type_attr or fi_pres: + obj = {'field': field, 'op': op} + if op not in no_value_ops and value is not None: + if value_is_array_flag: + obj['value'] = list(value) + 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: + obj['userSettingPresentation'] = get_ml_text(user_pres_node) + return obj + + s = field + if op in no_value_ops: + s += " %s" % op + else: + v_display = '_' + if value is not None: + if isinstance(value, bool): + v_display = 'true' if value else 'false' + elif str(value) != '': + v_display = str(value) + s += " %s %s" % (op, v_display) + if flags: + s += ' ' + ' '.join(flags) + return s + + +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.select_single_node("dcsset:field") + if field_el: + return 'Auto' + if xt == 'SelectedItemAuto': + use_v = get_text(item, "dcsset:use") + if use_v == 'false': + return {'auto': True, 'use': False} + return 'Auto' + if xt == 'SelectedItemField': + f_name = get_text(item, "dcsset:field") + title_node = item.select_single_node("dcsset:lwsTitle") + title = get_ml_text(title_node) + vm_n = item.select_single_node("dcsset:viewMode") + use_v = get_text(item, "dcsset:use") + use_false = (use_v == 'false') + if title or vm_n or use_false: + obj = {'field': f_name} + if use_false: + obj['use'] = False + if title: + obj['title'] = title + if vm_n: + obj['viewMode'] = vm_n.inner_text + return obj + return f_name + if xt == 'SelectedItemFolder': + title_node = item.select_single_node("dcsset:lwsTitle") + folder_title = get_ml_text(title_node) + inner = [] + for sub in item.select_nodes("dcsset:item"): + inner.append(build_selection_item(sub, "%s/folder" % loc)) + entry = {'folder': folder_title, 'items': inner} + folder_field = get_text(item, "dcsset:field") + if folder_field: + entry['field'] = folder_field + pl_n = item.select_single_node("dcsset:placement") + if pl_n and pl_n.inner_text and pl_n.inner_text != 'Auto': + entry['placement'] = pl_n.inner_text + return entry + return new_sentinel("SelectionItem:%s" % xt, loc, 'Неизвестный тип элемента selection') + + +def build_selection(sel_node, loc): + if not sel_node: + return [] + out = [] + for it in sel_node.select_nodes("dcsset:item"): + out.append(build_selection_item(it, loc)) + return out + + +def build_order(ord_node, loc): + if not ord_node: + return [] + out = [] + for it in ord_node.select_nodes("dcsset:item"): + 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.select_single_node("dcsset:viewMode") + use_v = get_text(it, "dcsset:use") + use_false = (use_v == 'false') + if vm_n or use_false: + obj = {'field': fn} + if use_false: + obj['use'] = False + if ot == 'Desc': + obj['direction'] = 'desc' + if vm_n: + obj['viewMode'] = vm_n.inner_text + out.append(obj) + else: + if ot == 'Desc': + out.append("%s desc" % fn) + else: + out.append(fn) + else: + out.append(new_sentinel("OrderItem:%s" % xt, loc, 'Неизвестный тип сортировки')) + return out + + +def get_line_value(val_node): + obj = {'@type': 'Line'} + w = val_node.el.get('width') + g = val_node.el.get('gap') + if w is not None and w != '': + obj['width'] = int(w) if re.match(r'^-?\d+$', w) else w + if g is not None and g != '': + obj['gap'] = (g == 'true') + style_node = val_node.select_single_node("v8ui:style") + if style_node: + obj['style'] = style_node.inner_text + return obj + + +def read_appearance_value_node(val_node): + if not val_node: + return None + vt = get_local_xsi_type(val_node) + if vt == 'LocalStringType': + return get_ml_text(val_node) + if vt == 'Font': + return get_font_value(val_node) + if vt == 'Line': + return get_line_value(val_node) + return val_node.inner_text + + +def get_settings_appearance(app_node): + if not app_node: + return None + d = {} + for it in app_node.select_nodes("dcscor:item"): + p_name = get_text(it, "dcscor:parameter") + val = it.select_single_node("dcscor:value") + if not p_name or not val: + continue + raw_val = read_appearance_value_node(val) + use_v = get_text(it, "dcscor:use") + nested_items = {} + for sub in it.select_nodes("dcscor:item"): + sub_name = get_text(sub, "dcscor:parameter") + sub_val = sub.select_single_node("dcscor:value") + if not sub_name: + continue + sub_raw = read_appearance_value_node(sub_val) + sub_use = get_text(sub, "dcscor:use") + sub_entry = {'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 = {'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 + + +def build_conditional_appearance(ca_node, loc): + if not ca_node: + return [] + out = [] + i = 0 + for it in ca_node.select_nodes("dcsset:item"): + entry = {} + scope_node = it.select_single_node("dcsset:scope") + if scope_node and scope_node.has_child_nodes: + add_warning('SilentDrop:scope', "%s/%d/scope" % (loc, i), "conditionalAppearance item имеет scope — не воспроизводится в DSL") + sel_node = it.select_single_node("dcsset:selection") + if sel_node and len(sel_node.select_nodes("dcsset:item")) > 0: + entry['selection'] = build_selection(sel_node, "%s/%d/selection" % (loc, i)) + filter_node = it.select_single_node("dcsset:filter") + if filter_node and len(filter_node.select_nodes("dcsset:item")) > 0: + f = [] + for fc in filter_node.select_nodes("dcsset:item"): + f.append(build_filter_item(fc, "%s/%d/filter" % (loc, i))) + entry['filter'] = f + app_node = it.select_single_node("dcsset:appearance") + ap = get_settings_appearance(app_node) + if ap and len(ap) > 0: + entry['appearance'] = ap + pres_node = it.select_single_node("dcsset:presentation") + if pres_node: + pres = get_ml_text(pres_node) + if not pres: + pres = pres_node.inner_text + if pres: + entry['presentation'] = pres + vm_n = it.select_single_node("dcsset:viewMode") + if vm_n: + entry['viewMode'] = vm_n.inner_text + usid = get_text(it, "dcsset:userSettingID") + if usid: + entry['userSettingID'] = 'auto' + usp_n = it.select_single_node("dcsset:userSettingPresentation") + if usp_n: + usp = get_ml_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 it.child_nodes: + if ch.namespace_uri != NS_SET: + continue + m = re.match(r'^useIn(.+)$', ch.local_name) + if m and ch.inner_text == 'DontUse': + tail = m.group(1) + short_name = tail[0].lower() + tail[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 + + +# Build outputParameters dict +output_param_types_known = { + 'Заголовок': 'mltext', + 'ВыводитьЗаголовок': 'dcsset:DataCompositionTextOutputType', + 'ВыводитьПараметрыДанных': 'dcsset:DataCompositionTextOutputType', + 'ВыводитьОтбор': 'dcsset:DataCompositionTextOutputType', + 'МакетОформления': 'xs:string', + 'РасположениеПолейГруппировки': 'dcsset:DataCompositionGroupFieldsPlacement', + 'РасположениеРеквизитов': 'dcsset:DataCompositionAttributesPlacement', + 'ГоризонтальноеРасположениеОбщихИтогов': 'dcscor:DataCompositionTotalPlacement', + 'ВертикальноеРасположениеОбщихИтогов': 'dcscor:DataCompositionTotalPlacement', + 'РасположениеОбщихИтогов': 'dcscor:DataCompositionTotalPlacement', + 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement', + 'РасположениеГруппировки': 'dcsset:DataCompositionFieldGroupPlacement', + 'РасположениеРесурсов': 'dcsset:DataCompositionResourcesPlacement', + 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType', +} + + +def build_output_parameters(op_node): + if not op_node: + return None + d = {} + for it in op_node.select_nodes("dcscor:item"): + p_name = get_text(it, "dcscor:parameter") + val = it.select_single_node("dcscor:value") + if not p_name or not val: + continue + v_type = get_local_xsi_type(val) + full_type = val.get_attr_raw('xsi:type') + if v_type == 'LocalStringType': + raw_val = get_ml_text(val) + elif v_type == 'Font': + raw_val = get_font_value(val) + else: + raw_val = val.inner_text + nested_items = {} + for sub in it.select_nodes("dcscor:item"): + sub_name = get_text(sub, "dcscor:parameter") + sub_val = sub.select_single_node("dcscor:value") + if not sub_name or not sub_val: + continue + sub_type = get_local_xsi_type(sub_val) + sub_full = sub_val.get_attr_raw('xsi:type') + if sub_type == 'LocalStringType': + sub_raw = get_ml_text(sub_val) + elif sub_type == 'Font': + sub_raw = get_font_value(sub_val) + else: + sub_raw = sub_val.inner_text + sub_type_field = sub_full + if sub_full: + m = re.match(r'^([^:]+):(.+)$', sub_full) + if m: + pfx = m.group(1) + local_name = m.group(2) + uri = sub_val.get_namespace_of_prefix(pfx) + if uri and uri not in ( + 'http://www.w3.org/2001/XMLSchema', + 'http://www.w3.org/2001/XMLSchema-instance', + 'http://v8.1c.ru/8.1/data-composition-system/schema', + 'http://v8.1c.ru/8.1/data-composition-system/settings', + 'http://v8.1c.ru/8.1/data-composition-system/core', + 'http://v8.1c.ru/8.1/data-composition-system/common', + 'http://v8.1c.ru/8.1/data/core', + 'http://v8.1c.ru/8.1/data/ui', + ): + sub_type_field = {'uri': uri, 'name': local_name} + entry = {'value': sub_raw, 'valueType': sub_type_field} + sub_use = get_text(sub, "dcscor:use") + if sub_use == 'false': + entry['use'] = False + nested_items[sub_name] = entry + use_v = get_text(it, "dcscor:use") + vm_n = it.select_single_node("dcsset:viewMode") + usid_v = get_text(it, "dcsset:userSettingID") + usp_n = it.select_single_node("dcsset:userSettingPresentation") + type_auto_detected = False + if p_name in output_param_types_known: + map_type = output_param_types_known[p_name] + if map_type == 'mltext' and v_type == 'LocalStringType': + type_auto_detected = True + elif full_type == map_type: + type_auto_detected = True + type_is_custom = bool(full_type) and (not re.match(r'^xs:', full_type)) and (v_type != 'LocalStringType') and (v_type != 'Font') and (not type_auto_detected) + has_extras = (use_v == 'false') or vm_n or usid_v or usp_n or (len(nested_items) > 0) or type_is_custom + if has_extras: + wrap = {'value': raw_val} + if full_type and not (v_type == 'LocalStringType' or v_type == 'Font') and not type_auto_detected: + wrap['valueType'] = full_type + if use_v == 'false': + wrap['use'] = False + if len(nested_items) > 0: + wrap['items'] = nested_items + if vm_n: + wrap['viewMode'] = vm_n.inner_text + if usid_v: + wrap['userSettingID'] = 'auto' + if usp_n: + wrap['userSettingPresentation'] = get_ml_text(usp_n) + d[p_name] = wrap + else: + d[p_name] = raw_val + return d + + +auto_dates_companions = {} + + +def build_data_parameters(dp_node, top_params): + if not dp_node: + return None + items = dp_node.select_nodes("dcscor:item") + if len(items) == 0: + return None + visible_top = {} + for tp in top_params: + if not tp['hidden'] and tp['name'] not in auto_dates_companions: + visible_top[tp['name']] = tp + can_auto = True + present_names = {} + entries = [] + for it in items: + pn = get_text(it, "dcscor:parameter") + present_names[pn] = True + usid = get_text(it, "dcsset:userSettingID") + if not usid: + can_auto = False + val_node = it.select_single_node("dcscor:value") + use = get_text(it, "dcscor:use") + if use == 'false': + can_auto = False + vm_n = it.select_single_node("dcsset:viewMode") + usp_n = it.select_single_node("dcsset:userSettingPresentation") + if vm_n or usp_n: + can_auto = False + tp = visible_top.get(pn) + flags = [] + if usid: + flags.append('@user') + if use == 'false': + flags.append('@off') + vt = get_local_xsi_type(val_node) + v_display = None + std_period_obj = None + if vt == 'StandardPeriod': + variant = get_text(val_node, "v8:variant") + sd = get_text(val_node, "v8:startDate") + ed = get_text(val_node, "v8:endDate") + has_explicit_dates = (sd and sd != '0001-01-01T00:00:00') or (ed and ed != '0001-01-01T00:00:00') + if has_explicit_dates: + std_period_obj = {'variant': variant} + if sd: + std_period_obj['startDate'] = sd + if ed: + std_period_obj['endDate'] = ed + can_auto = False + elif variant: + v_display = variant + if vm_n or usp_n: + std_period_obj = {'variant': variant} + elif vt == 'StandardBeginningDate': + variant = get_text(val_node, "v8:variant") + d_ = get_text(val_node, "v8:date") + has_explicit_date = d_ and d_ != '0001-01-01T00:00:00' + if has_explicit_date: + std_period_obj = {'variant': variant, 'date': d_} + can_auto = False + elif variant: + std_period_obj = {'variant': variant} + can_auto = False + elif vt == 'DesignTimeValue': + v_display = val_node.inner_text + elif vt == 'LocalStringType': + v_display = get_ml_text(val_node) + else: + if val_node: + v_display = val_node.inner_text + if tp and tp['valueDisplay'] != v_display: + can_auto = False + if not tp: + can_auto = False + is_empty_string_placeholder = (vt == 'string') and (not val_node.inner_text) and (use == 'false') + if is_empty_string_placeholder: + can_auto = False + if std_period_obj or vm_n or usp_n or is_empty_string_placeholder: + obj = {'parameter': pn} + if std_period_obj: + obj['value'] = std_period_obj + elif is_empty_string_placeholder: + obj['value'] = '' + obj['valueType'] = 'xs:string' + elif v_display is not None and v_display != '': + if vt == 'boolean': + obj['value'] = (v_display == 'true') + elif vt == 'decimal': + if re.match(r'^-?\d+$', v_display): + obj['value'] = int(v_display) + else: + obj['value'] = float(v_display) + else: + obj['value'] = v_display + ta = val_node.get_attr_raw('xsi:type') + if ta: + obj['valueType'] = ta + if use == 'false': + obj['use'] = False + if usid: + obj['userSettingID'] = 'auto' + if vm_n: + obj['viewMode'] = vm_n.inner_text + if usp_n: + usp_v = get_ml_text(usp_n) + if usp_v: + obj['userSettingPresentation'] = usp_v + entries.append(obj) + else: + s = pn + if v_display is not None and v_display != '': + s += " = %s" % v_display + if flags: + s += ' ' + ' '.join(flags) + entries.append(s) + for vn in visible_top: + if vn not in present_names: + can_auto = False + if can_auto: + return 'auto' + return entries + + +def get_group_fields(parent_node, loc): + g_fields = [] + gi = parent_node.select_single_node("dcsset:groupItems") + if not gi: + return g_fields + for g_item in gi.select_nodes("dcsset:item"): + gxt = get_local_xsi_type(g_item) + if gxt == 'GroupItemAuto': + g_fields.append('Auto') + elif gxt == 'GroupItemField': + gf = get_text(g_item, "dcsset:field") + pat = get_text(g_item, "dcsset:periodAdditionType") + gt = get_text(g_item, "dcsset:groupType") + pab_n = g_item.select_single_node("dcsset:periodAdditionBegin") + pae_n = g_item.select_single_node("dcsset:periodAdditionEnd") + pab = None + pae = None + if pab_n: + pt = get_local_xsi_type(pab_n) + pv = pab_n.inner_text + if pt == 'Field' or (pv and pv != '0001-01-01T00:00:00'): + pab = pv + if pae_n: + pt = get_local_xsi_type(pae_n) + pv = pae_n.inner_text + if pt == 'Field' or (pv and pv != '0001-01-01T00:00:00'): + pae = pv + is_default = (not pat or pat == 'None') and (not gt or gt == 'Items') and (not pab) and (not pae) + if is_default: + g_fields.append(gf) + else: + obj = {'field': gf} + if gt and gt != 'Items': + obj['groupType'] = gt + if pat and pat != 'None': + obj['periodAdditionType'] = pat + if pab: + obj['periodAdditionBegin'] = pab + if pae: + obj['periodAdditionEnd'] = pae + g_fields.append(obj) + else: + g_fields.append(new_sentinel("GroupItem:%s" % gxt, "%s/groupItems" % loc, 'Тип элемента группировки не покрыт')) + return g_fields + + +def build_table_axis_block(node, loc, include_name=False): + entry = {} + nm = get_text(node, "dcsset:name") + if nm: + entry['name'] = nm + gf = get_group_fields(node, loc) + if len(gf) > 0: + entry['groupFields'] = gf + f_node = node.select_single_node("dcsset:filter") + if f_node and len(f_node.select_nodes("dcsset:item")) > 0: + fa = [] + for fc in f_node.select_nodes("dcsset:item"): + fa.append(build_filter_item(fc, "%s/filter" % loc)) + entry['filter'] = fa + ord_node = node.select_single_node("dcsset:order") + if ord_node: + ord_items = build_order(ord_node, "%s/order" % loc) + if len(ord_items) > 0: + entry['order'] = ord_items + sel_node = node.select_single_node("dcsset:selection") + if sel_node: + sel_items = build_selection(sel_node, "%s/selection" % loc) + if len(sel_items) > 0: + entry['selection'] = sel_items + ca_n = node.select_single_node("dcsset:conditionalAppearance") + if ca_n: + ca = build_conditional_appearance(ca_n, "%s/ca" % loc) + if len(ca) > 0: + entry['conditionalAppearance'] = ca + op_node = node.select_single_node("dcsset:outputParameters") + op = build_output_parameters(op_node) + if op and len(op) > 0: + entry['outputParameters'] = op + children = build_structure(node, "%s/children" % loc) + if len(children) > 0: + entry['children'] = children + avm_node = node.select_single_node("dcsset:viewMode") + if avm_node: + entry['viewMode'] = avm_node.inner_text + ausid = get_text(node, "dcsset:userSettingID") + if ausid: + entry['userSettingID'] = 'auto' + aus_pres_node = node.select_single_node("dcsset:userSettingPresentation") + if aus_pres_node: + aus_pres = get_ml_text(aus_pres_node) + if aus_pres: + entry['userSettingPresentation'] = aus_pres + aivm_node = node.select_single_node("dcsset:itemsViewMode") + if aivm_node: + entry['itemsViewMode'] = aivm_node.inner_text + return entry + + +def build_structure(node, loc): + if not node: + return [] + items = [] + idx = 0 + for it in node.select_nodes("dcsset:item"): + xt = get_local_xsi_type(it) + if xt == 'StructureItemTable': + entry = {'type': 'table'} + nm = get_text(it, "dcsset:name") + if nm: + entry['name'] = nm + cols = [] + for cn in it.select_nodes("dcsset:column"): + cols.append(build_table_axis_block(cn, "%s/%d/column" % (loc, idx))) + if len(cols) > 0: + entry['columns'] = cols + rows = [] + for rn in it.select_nodes("dcsset:row"): + rows.append(build_table_axis_block(rn, "%s/%d/row" % (loc, idx), include_name=True)) + if len(rows) > 0: + entry['rows'] = rows + t_sel_n = it.select_single_node("dcsset:selection") + if t_sel_n: + t_sel_i = build_selection(t_sel_n, "%s/%d/selection" % (loc, idx)) + if len(t_sel_i) > 0: + entry['selection'] = t_sel_i + t_op_n = it.select_single_node("dcsset:outputParameters") + t_op = build_output_parameters(t_op_n) + if t_op and len(t_op) > 0: + entry['outputParameters'] = t_op + t_ca_n = it.select_single_node("dcsset:conditionalAppearance") + if t_ca_n: + t_ca = build_conditional_appearance(t_ca_n, "%s/%d/ca" % (loc, idx)) + if len(t_ca) > 0: + entry['conditionalAppearance'] = t_ca + t_use = get_text(it, "dcsset:use") + if t_use == 'false': + entry['use'] = False + for ch in it.child_nodes: + if ch.namespace_uri != NS_SET: + continue + if ch.local_name == 'viewMode' and 'viewMode' not in entry: + entry['viewMode'] = ch.inner_text + elif ch.local_name == 'userSettingID' and 'userSettingID' not in entry: + entry['userSettingID'] = 'auto' + elif ch.local_name == 'userSettingPresentation' and 'userSettingPresentation' not in entry: + usp_v = get_ml_text(ch) + if usp_v: + entry['userSettingPresentation'] = usp_v + elif ch.local_name == 'itemsViewMode' and 'itemsViewMode' not in entry: + entry['itemsViewMode'] = ch.inner_text + elif ch.local_name == 'columnsViewMode' and 'columnsViewMode' not in entry: + entry['columnsViewMode'] = ch.inner_text + elif ch.local_name == 'rowsViewMode' and 'rowsViewMode' not in entry: + entry['rowsViewMode'] = ch.inner_text + items.append(entry) + idx += 1 + continue + if xt == 'StructureItemNestedObject': + entry = {'type': 'nestedObject'} + obj_id = get_text(it, "dcsset:objectID") + if obj_id: + entry['objectID'] = obj_id + settings_node = it.select_single_node("dcsset:settings") + if settings_node: + nested_settings = {} + sel_node = settings_node.select_single_node("dcsset:selection") + sel_i = build_selection(sel_node, "%s/%d/nested/selection" % (loc, idx)) + if len(sel_i) > 0: + nested_settings['selection'] = sel_i + f_node = settings_node.select_single_node("dcsset:filter") + if f_node and len(f_node.select_nodes("dcsset:item")) > 0: + fa = [] + for fc in f_node.select_nodes("dcsset:item"): + fa.append(build_filter_item(fc, "%s/%d/nested/filter" % (loc, idx))) + nested_settings['filter'] = fa + o_node = settings_node.select_single_node("dcsset:order") + o_i = build_order(o_node, "%s/%d/nested/order" % (loc, idx)) + if len(o_i) > 0: + nested_settings['order'] = o_i + ca_node = settings_node.select_single_node("dcsset:conditionalAppearance") + if ca_node: + ca = build_conditional_appearance(ca_node, "%s/%d/nested/ca" % (loc, idx)) + if len(ca) > 0: + nested_settings['conditionalAppearance'] = ca + op_node = settings_node.select_single_node("dcsset:outputParameters") + op = build_output_parameters(op_node) + if op and len(op) > 0: + nested_settings['outputParameters'] = op + entry['settings'] = nested_settings + items.append(entry) + idx += 1 + continue + if xt == 'StructureItemChart': + entry = {'type': 'chart'} + nm = get_text(it, "dcsset:name") + if nm: + entry['name'] = nm + pn_list = it.select_nodes("dcsset:point") + if len(pn_list) == 1: + entry['points'] = build_table_axis_block(pn_list[0], "%s/%d/point" % (loc, idx)) + elif len(pn_list) > 1: + p_arr = [] + pi = 0 + for p in pn_list: + p_arr.append(build_table_axis_block(p, "%s/%d/point[%d]" % (loc, idx, pi))) + pi += 1 + entry['points'] = p_arr + sn_list = it.select_nodes("dcsset:series") + if len(sn_list) == 1: + entry['series'] = build_table_axis_block(sn_list[0], "%s/%d/series" % (loc, idx)) + elif len(sn_list) > 1: + s_arr = [] + si = 0 + for s in sn_list: + s_arr.append(build_table_axis_block(s, "%s/%d/series[%d]" % (loc, idx, si))) + si += 1 + entry['series'] = s_arr + sel_n = it.select_single_node("dcsset:selection") + if sel_n: + sel_i = build_selection(sel_n, "%s/%d/selection" % (loc, idx)) + if len(sel_i) > 0: + entry['selection'] = sel_i + op_n = it.select_single_node("dcsset:outputParameters") + op = build_output_parameters(op_n) + if op and len(op) > 0: + entry['outputParameters'] = op + ch_use = get_text(it, "dcsset:use") + if ch_use == 'false': + entry['use'] = False + for ch in it.child_nodes: + if ch.namespace_uri != NS_SET: + continue + if ch.local_name == 'viewMode' and 'viewMode' not in entry: + entry['viewMode'] = ch.inner_text + elif ch.local_name == 'userSettingID' and 'userSettingID' not in entry: + entry['userSettingID'] = 'auto' + elif ch.local_name == 'userSettingPresentation' and 'userSettingPresentation' not in entry: + usp_v = get_ml_text(ch) + if usp_v: + entry['userSettingPresentation'] = usp_v + elif ch.local_name == 'itemsViewMode' and 'itemsViewMode' not in entry: + entry['itemsViewMode'] = ch.inner_text + elif ch.local_name == 'pointsViewMode' and 'pointsViewMode' not in entry: + entry['pointsViewMode'] = ch.inner_text + elif ch.local_name == 'seriesViewMode' and 'seriesViewMode' not in entry: + entry['seriesViewMode'] = ch.inner_text + items.append(entry) + idx += 1 + continue + if xt and xt != 'StructureItemGroup': + items.append(new_sentinel("StructureItem:%s" % xt, loc, 'Тип структуры пока не покрыт')) + idx += 1 + continue + entry = {} + g_use = get_text(it, "dcsset:use") + if g_use == 'false': + entry['use'] = False + nm = get_text(it, "dcsset:name") + if nm: + entry['name'] = nm + g_fields = get_group_fields(it, loc) + if len(g_fields) > 0: + entry['groupFields'] = g_fields + + sel_node = it.select_single_node("dcsset:selection") + if sel_node: + sel_items = build_selection(sel_node, "%s/selection" % loc) + if len(sel_items) > 0: + entry['selection'] = sel_items + ord_node = it.select_single_node("dcsset:order") + if ord_node: + ord_items = build_order(ord_node, "%s/order" % loc) + if len(ord_items) > 0: + entry['order'] = ord_items + for ch in ord_node.child_nodes: + if ch.namespace_uri != NS_SET: + continue + if ch.local_name == 'viewMode': + entry['orderViewMode'] = ch.inner_text + elif ch.local_name == 'userSettingID': + entry['orderUserSettingID'] = 'auto' + filter_node = it.select_single_node("dcsset:filter") + if filter_node and len(filter_node.select_nodes("dcsset:item")) > 0: + f = [] + for fc in filter_node.select_nodes("dcsset:item"): + f.append(build_filter_item(fc, "%s/filter" % loc)) + entry['filter'] = f + ca_node = it.select_single_node("dcsset:conditionalAppearance") + if ca_node: + ca = build_conditional_appearance(ca_node, "%s/ca" % loc) + if len(ca) > 0: + entry['conditionalAppearance'] = ca + op_node = it.select_single_node("dcsset:outputParameters") + op = build_output_parameters(op_node) + if op and len(op) > 0: + entry['outputParameters'] = op + + children = build_structure(it, "%s/children" % loc) + if len(children) > 0: + entry['children'] = children + + gvm = None + givm = None + gusid = None + gusp_node = None + for ch in it.child_nodes: + if ch.namespace_uri != NS_SET: + continue + if ch.local_name == 'viewMode' and gvm is None: + gvm = ch.inner_text + elif ch.local_name == 'itemsViewMode' and givm is None: + givm = ch.inner_text + elif ch.local_name == 'userSettingID' and gusid is None: + gusid = ch.inner_text + elif ch.local_name == 'userSettingPresentation' and gusp_node is None: + gusp_node = ch + if gvm is not None: + entry['viewMode'] = gvm + if givm is not None: + entry['itemsViewMode'] = givm + if gusid: + entry['userSettingID'] = 'auto' + if gusp_node: + gusp = get_ml_text(gusp_node) + if gusp: + entry['userSettingPresentation'] = gusp + + items.append(entry) + idx += 1 + return items + + +def try_structure_shorthand(items): + if len(items) != 1: + return None + parts = [] + cur = items[0] + while cur is not None: + if 'type' in cur and cur['type'] != 'group': + return None + if 'name' in cur: + return None + if 'selection' in cur: + return None + if 'order' in cur: + return None + if 'filter' in cur: + return None + if 'viewMode' in cur: + return None + if 'itemsViewMode' in cur: + return None + if 'userSettingID' in cur: + return None + if 'userSettingPresentation' in cur: + return None + if 'use' in cur: + return None + if 'conditionalAppearance' in cur: + return None + if 'outputParameters' in cur: + return None + gfs = cur.get('groupFields') + if gfs is None or len(gfs) == 0: + parts.append('details') + break + if len(gfs) != 1: + return None + if not isinstance(gfs[0], str): + return None + parts.append(gfs[0]) + children = cur.get('children') + if children is None or len(children) == 0: + break + if len(children) != 1: + return None + cur = children[0] + return ' > '.join(parts) + + +# --- 4. dataSources / 5. dataSets / ... main runner --- + +def build_data_set(ds_node, loc, emit_data_sources): + xsi_type = get_local_xsi_type(ds_node) + name = get_text(ds_node, "r:name") + ds = {'name': name} + + if xsi_type == 'DataSetQuery': + query_text = get_text(ds_node, "r:query") + ds['query'] = maybe_externalize_query(query_text, name) + elif xsi_type == 'DataSetObject': + ds['objectName'] = get_text(ds_node, "r:objectName") + elif xsi_type == 'DataSetUnion': + nested = [] + ni = 0 + inner_nodes = list(ds_node.select_nodes("r:item")) + list(ds_node.select_nodes("r:dataSet")) + for n_node in inner_nodes: + nested.append(build_data_set(n_node, "%s/items[%d]" % (loc, ni), emit_data_sources)) + ni += 1 + ds['items'] = nested + else: + ds['__unsupported__'] = new_sentinel("DataSetType:%s" % xsi_type, loc, "Неизвестный тип набора данных")['__unsupported__'] + + field_nodes = ds_node.select_nodes("r:field") + if len(field_nodes) > 0: + fields = [] + fi = 0 + for fn in field_nodes: + fxsi = get_local_xsi_type(fn) + if fxsi == 'DataSetFieldField': + fields.append(build_field(fn, "%s/field[%d]" % (loc, fi))) + elif fxsi == 'DataSetFieldFolder': + folder_obj = { + 'field': get_text(fn, "r:dataPath"), + 'folder': True, + } + title_node = fn.select_single_node("r:title") + title = get_ml_text(title_node) + if title: + folder_obj['title'] = title + fields.append(folder_obj) + else: + fields.append(new_sentinel("FieldType:%s" % fxsi, "%s/field[%d]" % (loc, fi), 'Тип поля не DataSetFieldField/Folder')) + fi += 1 + ds['fields'] = fields + + if xsi_type != 'DataSetUnion': + ds_src = get_text(ds_node, "r:dataSource") + if emit_data_sources and ds_src: + ds['dataSource'] = ds_src + + return ds + + +def run(root): + global output_dir, output_basename, auto_dates_companions + + # Ring 3 scans + for el in root.el.iter(): + local = el.tag.split('}', 1)[1] if '}' in el.tag else el.tag + if local == 'item': + xsi = el.get('{%s}type' % NS_XSI) or '' + ns_uri = el.tag[1:].split('}', 1)[0] if el.tag.startswith('{') else '' + if re.search(r'Picture$', xsi) and ns_uri == NS_AT: + fail_ring3("Picture cell в шаблоне", "template/.../item[@xsi:type=Picture]") + if local == 'Type': + inner = _inner_text(el) + if re.search(r'^v8:ValueStorage$|:ValueStorage$', inner or ''): + fail_ring3("параметр типа ХранилищеЗначения", "valueType[v8:Type=ValueStorage]") + if local == 'templateCondition': + fail_ring3("templateCondition (вариативные шаблоны)", "template/templateCondition") + if local == 'nestedSchema': + fail_ring3("nestedSchema (вложенные подсхемы)", "nestedSchema") + + ds_count_check = 0 + for el in root.el.iter(): + local = el.tag.split('}', 1)[1] if '}' in el.tag else el.tag + if local == 'dataSet': + ds_count_check += 1 + if ds_count_check == 0: + fail_ring3("отчёт без dataSet (служебный шаблон-обёртка)", "DataCompositionSchema/dataSet") + + # Resolve outputPath + load user styles + global OutputPath + if OutputPath: + if not os.path.isabs(OutputPath): + OutputPath = os.path.join(os.getcwd(), OutputPath) + output_dir = os.path.dirname(OutputPath) + output_basename = os.path.splitext(os.path.basename(OutputPath))[0] + load_user_styles(output_dir) + + data_sources = [] + dsource_nodes = root.select_nodes("r:dataSource") + for dsn in dsource_nodes: + nm = get_text(dsn, "r:name") + tp = get_text(dsn, "r:dataSourceType") + data_sources.append({'name': nm, 'type': tp}) + emit_data_sources = True + if len(data_sources) == 1 and data_sources[0]['name'] == 'ИсточникДанных1' and data_sources[0]['type'] == 'Local': + emit_data_sources = False + + # --- 5. dataSets --- + data_sets = [] + ds_nodes = root.select_nodes("r:dataSet") + dsi = 0 + for ds_node in ds_nodes: + data_sets.append(build_data_set(ds_node, "dataSet[%d]" % dsi, emit_data_sources)) + dsi += 1 + + # --- 5a-bis. dataSetLinks --- + data_set_links = [] + dsl_nodes = root.select_nodes("r:dataSetLink") + for dsl_node in dsl_nodes: + link = {} + link['sourceDataSet'] = get_text(dsl_node.select_single_node("r:sourceDataSet"), '.') if False else (dsl_node.select_single_node("r:sourceDataSet").inner_text if dsl_node.select_single_node("r:sourceDataSet") else None) + # PS code uses Get-Text $node (without xpath) — for our wrapper, just read inner_text + s_n = dsl_node.select_single_node("r:sourceDataSet") + d_n = dsl_node.select_single_node("r:destinationDataSet") + se_n = dsl_node.select_single_node("r:sourceExpression") + de_n = dsl_node.select_single_node("r:destinationExpression") + link['sourceDataSet'] = s_n.inner_text if s_n else None + link['destinationDataSet'] = d_n.inner_text if d_n else None + link['sourceExpression'] = se_n.inner_text if se_n else None + link['destinationExpression'] = de_n.inner_text if de_n else None + p_node = dsl_node.select_single_node("r:parameter") + if p_node: + link['parameter'] = p_node.inner_text + pla_node = dsl_node.select_single_node("r:parameterListAllowed") + if pla_node and pla_node.inner_text == 'true': + link['parameterListAllowed'] = True + se_node = dsl_node.select_single_node("r:startExpression") + if se_node: + link['startExpression'] = se_node.inner_text + lce_node = dsl_node.select_single_node("r:linkConditionExpression") + if lce_node: + link['linkConditionExpression'] = lce_node.inner_text + data_set_links.append(link) + + # --- 5b. calculatedFields --- + calculated_fields = [] + cf_nodes = root.select_nodes("r:calculatedField") + ci = 0 + for cf in cf_nodes: + calculated_fields.append(build_calc_field(cf, "calculatedField[%d]" % ci)) + ci += 1 + + # --- 5c. totalFields --- + total_fields = [] + tf_nodes = root.select_nodes("r:totalField") + for tf in tf_nodes: + total_fields.append(build_total_field(tf)) + + # --- 5d. parameters with autoDates folding --- + params_raw = [] + pi = 0 + p_nodes = root.select_nodes("r:parameter") + for p in p_nodes: + params_raw.append(build_parameter(p, "parameter[%d]" % pi)) + pi += 1 + + param_by_name = {} + for p in params_raw: + param_by_name[p['name']] = p + + removed_names = {} + auto_dates_companions.clear() + for p in params_raw: + if p['typeShort'] != 'StandardPeriod': + continue + parent_name = p['name'] + start_expr = '&' + parent_name + '.ДатаНачала' + end_expr = '&' + parent_name + '.ДатаОкончания' + start_match = None + end_match = None + for q in params_raw: + if q['name'] == parent_name: + continue + if q['expression'] == start_expr: + start_match = q['name'] + elif q['expression'] == end_expr: + end_match = q['name'] + begin_p = param_by_name.get(start_match) + end_p = param_by_name.get(end_match) + has_not_a_field = (begin_p and begin_p['notAField']) or (end_p and end_p['notAField']) + if start_match == 'НачалоПериода' and end_match == 'КонецПериода' and not has_not_a_field: + p['autoDates'] = True + removed_names[start_match] = True + removed_names[end_match] = True + auto_dates_companions[start_match] = True + auto_dates_companions[end_match] = True + + parameters = [] + for p in params_raw: + if p['name'] in removed_names: + continue + parameters.append(render_parameter(p)) + + # --- 6. Build top-level JSON object --- + out = {} + if emit_data_sources: + out['dataSources'] = data_sources + out['dataSets'] = data_sets + if len(data_set_links) > 0: + out['dataSetLinks'] = data_set_links + if len(calculated_fields) > 0: + out['calculatedFields'] = calculated_fields + if len(total_fields) > 0: + out['totalFields'] = total_fields + if len(parameters) > 0: + out['parameters'] = parameters + + # --- 5e. templates --- + templates = [] + t_nodes = root.select_nodes("r:template") + ti = 0 + for tn in t_nodes: + templates.append(build_template(tn, "template[%d]" % ti)) + ti += 1 + if len(templates) > 0: + out['templates'] = templates + + # --- 5e2. fieldTemplates --- + field_templates = [] + for ftn in root.select_nodes("r:fieldTemplate"): + ft_field = get_text(ftn, "r:field") + ft_templ = get_text(ftn, "r:template") + field_templates.append({'field': ft_field, 'template': ft_templ}) + if len(field_templates) > 0: + out['fieldTemplates'] = field_templates + + # --- 5f. groupTemplates --- + group_templates = [] + for ght in root.select_nodes("r:groupHeaderTemplate"): + entry = {} + gn = get_text(ght, "r:groupName") + gf = get_text(ght, "r:groupField") + if gn: + entry['groupName'] = gn + if gf: + entry['groupField'] = gf + entry['templateType'] = 'GroupHeader' + entry['template'] = get_text(ght, "r:template") + group_templates.append(entry) + for gt in root.select_nodes("r:groupTemplate"): + entry = {} + gn = get_text(gt, "r:groupName") + gf = get_text(gt, "r:groupField") + if gn: + entry['groupName'] = gn + if gf: + entry['groupField'] = gf + entry['templateType'] = get_text(gt, "r:templateType") + entry['template'] = get_text(gt, "r:template") + group_templates.append(entry) + if len(group_templates) > 0: + out['groupTemplates'] = group_templates + + # --- 5g. settingsVariants --- + def get_block_vm(node): + if not node: + return None + for child in node.child_nodes: + if child.local_name == 'viewMode' and child.namespace_uri == NS_SET: + return child.inner_text + return None + + def get_block_usid(node): + if not node: + return None + for child in node.child_nodes: + if child.local_name == 'userSettingID' and child.namespace_uri == NS_SET: + return child.inner_text + return None + + settings_variants = [] + sv_nodes = root.select_nodes("r:settingsVariant") + vi = 0 + for sv in sv_nodes: + vname = get_text(sv, "dcsset:name") + pres_node = sv.select_single_node("dcsset:presentation") + presentation = get_ml_text(pres_node) + + settings_node = sv.select_single_node("dcsset:settings") + settings = {} + + # userFields + uf_node = settings_node.select_single_node("dcsset:userFields") if settings_node else None + if uf_node: + uf_list = [] + ufi = 0 + for uf_item in uf_node.select_nodes("dcsset:item"): + uxt = get_local_xsi_type(uf_item) + entry = {} + dp = get_text(uf_item, "dcsset:dataPath") + if dp: + entry['dataPath'] = dp + title_n = uf_item.select_single_node("dcsset:lwsTitle") + title_v = get_ml_text(title_n) + if title_v: + entry['title'] = title_v + if uxt == 'UserFieldExpression': + d_ex_n = uf_item.select_single_node("dcsset:detailExpression") + d_ep_n = uf_item.select_single_node("dcsset:detailExpressionPresentation") + t_ex_n = uf_item.select_single_node("dcsset:totalExpression") + t_ep_n = uf_item.select_single_node("dcsset:totalExpressionPresentation") + if d_ex_n or d_ep_n: + d = {} + if d_ex_n: + d['expression'] = d_ex_n.inner_text + if d_ep_n: + d['presentation'] = d_ep_n.inner_text + entry['detail'] = d + if t_ex_n or t_ep_n: + t = {} + if t_ex_n: + t['expression'] = t_ex_n.inner_text + if t_ep_n: + t['presentation'] = t_ep_n.inner_text + entry['total'] = t + elif uxt == 'UserFieldCase': + cases_node = uf_item.select_single_node("dcsset:cases") + cases_arr = [] + if cases_node: + for case_item in cases_node.select_nodes("dcsset:item"): + ce = {} + cf_node2 = case_item.select_single_node("dcsset:filter") + if cf_node2 and len(cf_node2.select_nodes("dcsset:item")) > 0: + cfa = [] + for cfi in cf_node2.select_nodes("dcsset:item"): + cfa.append(build_filter_item(cfi, "variant[%d]/userField/case/filter" % vi)) + ce['filter'] = cfa + cv_node = case_item.select_single_node("dcsset:value") + if cv_node: + cv_type = get_local_xsi_type(cv_node) + cv_text = cv_node.inner_text + if cv_type == 'boolean': + ce['value'] = (cv_text == 'true') + elif cv_type == 'decimal': + if re.match(r'^-?\d+$', cv_text or ''): + ce['value'] = int(cv_text) + else: + ce['value'] = float(cv_text) + else: + ce['value'] = cv_text + cp_node = case_item.select_single_node("dcsset:lwsPresentationValue") + cp_v = get_ml_text(cp_node) + if cp_v: + ce['presentation'] = cp_v + cases_arr.append(ce) + entry['cases'] = cases_arr + else: + entry['__unsupported__'] = new_sentinel("UserField:%s" % uxt, "variant[%d]/userField[%d]" % (vi, ufi), 'Неизвестный тип пользовательского поля')['__unsupported__'] + uf_list.append(entry) + ufi += 1 + if len(uf_list) > 0: + settings['userFields'] = uf_list + + # selection (top-level) + sel_top = settings_node.select_single_node("dcsset:selection") if settings_node else None + sel_items = build_selection(sel_top, "variant[%d]/selection" % vi) + if len(sel_items) > 0: + settings['selection'] = sel_items + svm = get_block_vm(sel_top) + if svm is not None: + settings['selectionViewMode'] = svm + susid = get_block_usid(sel_top) + if susid: + settings['selectionUserSettingID'] = 'auto' + + # filter + f_top = settings_node.select_single_node("dcsset:filter") if settings_node else None + if f_top and len(f_top.select_nodes("dcsset:item")) > 0: + fa = [] + for fc in f_top.select_nodes("dcsset:item"): + fa.append(build_filter_item(fc, "variant[%d]/filter" % vi)) + settings['filter'] = fa + fvm = get_block_vm(f_top) + if fvm is not None: + settings['filterViewMode'] = fvm + fusid = get_block_usid(f_top) + if fusid: + settings['filterUserSettingID'] = 'auto' + + # order + ord_top = settings_node.select_single_node("dcsset:order") if settings_node else None + ord_items = build_order(ord_top, "variant[%d]/order" % vi) + if len(ord_items) > 0: + settings['order'] = ord_items + ovm = get_block_vm(ord_top) + if ovm is not None: + settings['orderViewMode'] = ovm + ousid = get_block_usid(ord_top) + if ousid: + settings['orderUserSettingID'] = 'auto' + + # conditionalAppearance + ca_top = settings_node.select_single_node("dcsset:conditionalAppearance") if settings_node else None + if ca_top: + ca = build_conditional_appearance(ca_top, "variant[%d]/ca" % vi) + if len(ca) > 0: + settings['conditionalAppearance'] = ca + cavm = get_block_vm(ca_top) + if cavm is not None: + settings['conditionalAppearanceViewMode'] = cavm + causid = get_block_usid(ca_top) + if causid: + settings['conditionalAppearanceUserSettingID'] = 'auto' + + # outputParameters + op_top = settings_node.select_single_node("dcsset:outputParameters") if settings_node else None + op = build_output_parameters(op_top) + if op and len(op) > 0: + settings['outputParameters'] = op + + # dataParameters + dp_top = settings_node.select_single_node("dcsset:dataParameters") if settings_node else None + dp = build_data_parameters(dp_top, params_raw) + if dp is not None: + settings['dataParameters'] = dp + + # structure + struct_items = build_structure(settings_node, "variant[%d]/structure" % vi) if settings_node else [] + if len(struct_items) > 0: + short = try_structure_shorthand(struct_items) + if short: + settings['structure'] = short + else: + settings['structure'] = struct_items + + sivm_node = settings_node.select_single_node("dcsset:itemsViewMode") if settings_node else None + if sivm_node: + settings['itemsViewMode'] = sivm_node.inner_text + + # additionalProperties + ap_node = settings_node.select_single_node("dcsset:additionalProperties") if settings_node else None + if ap_node: + ap_dict = {} + for prop in ap_node.select_nodes("v8:Property"): + p_name = prop.el.get("name") + val_el = prop.select_single_node("v8:Value") + if p_name and val_el: + ap_dict[p_name] = val_el.inner_text + if len(ap_dict) > 0: + settings['additionalProperties'] = ap_dict + + non_struct_keys = [k for k in settings if k != 'structure'] + struct_only_details = ('structure' not in settings) or (settings.get('structure') == 'details') + is_default = (len(non_struct_keys) == 0) and struct_only_details and (vname == 'Основной') and (not presentation or presentation == vname) + if not is_default: + entry = {'name': vname} + if presentation and presentation != vname: + entry['title'] = presentation + entry['settings'] = settings + settings_variants.append(entry) + vi += 1 + if len(settings_variants) > 0: + out['settingsVariants'] = settings_variants + + # --- 7. Serialize --- + json_text = convert_to_compact_json(out) + + if OutputPath: + with open(OutputPath, 'w', encoding='utf-8', newline='') as f: + f.write(json_text) + save_user_styles(output_dir) + save_query_files() + + if len(warnings_list) > 0: + base, _ext = os.path.splitext(OutputPath) + w_path = base + '.warnings.md' + sb = [] + sb.append("# skd-decompile warnings") + sb.append("") + sb.append("Source: %s" % TemplatePath) + sb.append("") + for w in warnings_list: + sb.append("- **%s** (%s) at %s — %s" % (w['id'], w['kind'], w['loc'], w['detail'])) + with open(w_path, 'w', encoding='utf-8', newline='') as f: + f.write('\r\n'.join(sb) + '\r\n') + print("Warnings: %s (%d issue(s))" % (w_path, len(warnings_list))) + + print("Decompiled: dataSets=%d, calc=%d, totals=%d, params=%d, templates=%d, groupTemplates=%d, variants=%d, warnings=%d" % ( + len(data_sets), len(calculated_fields), len(total_fields), len(parameters), len(templates), len(group_templates), len(settings_variants), len(warnings_list) + ), file=sys.stderr) + else: + print(json_text) + if len(warnings_list) > 0: + print("Warnings (%d):" % len(warnings_list), file=sys.stderr) + for w in warnings_list: + print(" %s [%s] %s: %s" % (w['id'], w['kind'], w['loc'], w['detail']), file=sys.stderr) + + +if __name__ == '__main__': + main()