#!/usr/bin/env python3 # form-compile v1.0 — Compile 1C managed form from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json import os import re import sys import uuid def esc_xml(s): return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') def emit_mltext(lines, indent, tag, text): if not text: lines.append(f"{indent}<{tag}/>") return lines.append(f"{indent}<{tag}>") lines.append(f"{indent}\t") lines.append(f"{indent}\t\tru") lines.append(f"{indent}\t\t{esc_xml(text)}") lines.append(f"{indent}\t") lines.append(f"{indent}") def new_uuid(): return str(uuid.uuid4()) def write_utf8_bom(path, content): with open(path, 'w', encoding='utf-8-sig', newline='') as f: f.write(content) # --- ID allocator --- _next_id = 0 def new_id(): global _next_id _next_id += 1 return _next_id # --- Event handler name generator --- EVENT_SUFFIX_MAP = { "OnChange": "\u041f\u0440\u0438\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", "StartChoice": "\u041d\u0430\u0447\u0430\u043b\u043e\u0412\u044b\u0431\u043e\u0440\u0430", "ChoiceProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u0412\u044b\u0431\u043e\u0440\u0430", "AutoComplete": "\u0410\u0432\u0442\u043e\u041f\u043e\u0434\u0431\u043e\u0440", "Clearing": "\u041e\u0447\u0438\u0441\u0442\u043a\u0430", "Opening": "\u041e\u0442\u043a\u0440\u044b\u0442\u0438\u0435", "Click": "\u041d\u0430\u0436\u0430\u0442\u0438\u0435", "OnActivateRow": "\u041f\u0440\u0438\u0410\u043a\u0442\u0438\u0432\u0438\u0437\u0430\u0446\u0438\u0438\u0421\u0442\u0440\u043e\u043a\u0438", "BeforeAddRow": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f", "BeforeDeleteRow": "\u041f\u0435\u0440\u0435\u0434\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c", "BeforeRowChange": "\u041f\u0435\u0440\u0435\u0434\u041d\u0430\u0447\u0430\u043b\u043e\u043c\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f", "OnStartEdit": "\u041f\u0440\u0438\u041d\u0430\u0447\u0430\u043b\u0435\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", "OnEndEdit": "\u041f\u0440\u0438\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0438\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", "Selection": "\u0412\u044b\u0431\u043e\u0440\u0421\u0442\u0440\u043e\u043a\u0438", "OnCurrentPageChange": "\u041f\u0440\u0438\u0421\u043c\u0435\u043d\u0435\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b", "TextEditEnd": "\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u0435\u0412\u0432\u043e\u0434\u0430\u0422\u0435\u043a\u0441\u0442\u0430", "URLProcessing": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439\u0421\u0441\u044b\u043b\u043a\u0438", "DragStart": "\u041d\u0430\u0447\u0430\u043b\u043e\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", "Drag": "\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435", "DragCheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u041f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u044f", "Drop": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435", "AfterDeleteRow": "\u041f\u043e\u0441\u043b\u0435\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f", } KNOWN_EVENTS = { "input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"], "check": ["OnChange"], "label": ["Click", "URLProcessing"], "labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"], "table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"], "pages": ["OnCurrentPageChange"], "page": ["OnCurrentPageChange"], "button": ["Click"], "picField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "Clearing"], "calendar": ["OnChange", "OnActivate"], "picture": ["Click"], "cmdBar": [], "popup": [], "group": [], } KNOWN_FORM_EVENTS = [ "OnCreateAtServer", "OnOpen", "BeforeClose", "OnClose", "NotificationProcessing", "ChoiceProcessing", "OnReadAtServer", "AfterWriteAtServer", "BeforeWriteAtServer", "AfterWrite", "BeforeWrite", "OnWriteAtServer", "FillCheckProcessingAtServer", "OnLoadDataFromSettingsAtServer", "BeforeLoadDataFromSettingsAtServer", "OnSaveDataInSettingsAtServer", "ExternalEvent", "OnReopen", "Opening", ] KNOWN_KEYS = { "group", "input", "check", "label", "labelField", "table", "pages", "page", "button", "picture", "picField", "calendar", "cmdBar", "popup", "name", "path", "title", "visible", "hidden", "enabled", "disabled", "readOnly", "on", "handlers", "titleLocation", "representation", "width", "height", "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", "multiLine", "passwordMode", "choiceButton", "clearButton", "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", "hyperlink", "showTitle", "united", "children", "columns", "changeRowSet", "changeRowOrder", "header", "footer", "commandBarLocation", "searchStringLocation", "pagesRepresentation", "type", "command", "stdCommand", "defaultButton", "locationInCommandBar", "src", "autofill", } TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page", "button", "picture", "picField", "calendar", "cmdBar", "popup"] def get_handler_name(element_name, event_name): suffix = EVENT_SUFFIX_MAP.get(event_name) if suffix: return f"{element_name}{suffix}" return f"{element_name}{event_name}" def get_element_name(el, type_key): if el.get('name'): return str(el['name']) return str(el.get(type_key, '')) def emit_events(lines, el, element_name, indent, type_key): if not el.get('on'): return # Validate event names if type_key and type_key in KNOWN_EVENTS: allowed = KNOWN_EVENTS[type_key] for evt in el['on']: if allowed and str(evt) not in allowed: print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") lines.append(f"{indent}") for evt in el['on']: evt_name = str(evt) handlers = el.get('handlers') if handlers and handlers.get(evt_name): handler = str(handlers[evt_name]) else: handler = get_handler_name(element_name, evt_name) lines.append(f'{indent}\t{handler}') lines.append(f"{indent}") def emit_companion(lines, tag, name, indent): cid = new_id() lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') def emit_common_flags(lines, el, indent): if el.get('visible') is False or el.get('hidden') is True: lines.append(f"{indent}false") if el.get('enabled') is False or el.get('disabled') is True: lines.append(f"{indent}false") if el.get('readOnly') is True: lines.append(f"{indent}true") def emit_title(lines, el, name, indent): if el.get('title'): emit_mltext(lines, indent, 'Title', str(el['title'])) # --- Type emitter --- V8_TYPES = { "ValueTable": "v8:ValueTable", "ValueTree": "v8:ValueTree", "ValueList": "v8:ValueListType", "TypeDescription": "v8:TypeDescription", "Universal": "v8:Universal", "FixedArray": "v8:FixedArray", "FixedStructure": "v8:FixedStructure", } UI_TYPES = { "FormattedString": "v8ui:FormattedString", "Picture": "v8ui:Picture", "Color": "v8ui:Color", "Font": "v8ui:Font", } DCS_MAP = { "DataCompositionSettings": "dcsset:DataCompositionSettings", "DataCompositionSchema": "dcssch:DataCompositionSchema", "DataCompositionComparisonType": "dcscor:DataCompositionComparisonType", } CFG_REF_PATTERN = re.compile( r'^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|' r'ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|' r'ExchangePlanRef|BusinessProcessRef|TaskRef|' r'InformationRegisterRecordSet|AccumulationRegisterRecordSet|DataProcessorObject)\.' ) _FORM_TYPE_SYNONYMS = { "строка": "string", "число": "decimal", "булево": "boolean", "дата": "date", "датавремя": "dateTime", "number": "decimal", "bool": "boolean", "справочникссылка": "CatalogRef", "справочникобъект": "CatalogObject", "документссылка": "DocumentRef", "документобъект": "DocumentObject", "перечислениессылка": "EnumRef", "плансчетовссылка": "ChartOfAccountsRef", "планвидовхарактеристикссылка": "ChartOfCharacteristicTypesRef", "планвидоврасчётассылка": "ChartOfCalculationTypesRef", "планвидоврасчетассылка": "ChartOfCalculationTypesRef", "планобменассылка": "ExchangePlanRef", "бизнеспроцессссылка": "BusinessProcessRef", "задачассылка": "TaskRef", "определяемыйтип": "DefinedType", } def resolve_type_str(type_str): if not type_str: return type_str m = re.match(r'^([^(]+)\((.+)\)$', type_str) if m: base, params = m.group(1).strip(), m.group(2) r = _FORM_TYPE_SYNONYMS.get(base.lower()) return f"{r}({params})" if r else type_str if '.' in type_str: i = type_str.index('.') prefix, suffix = type_str[:i], type_str[i:] r = _FORM_TYPE_SYNONYMS.get(prefix.lower()) return f"{r}{suffix}" if r else type_str r = _FORM_TYPE_SYNONYMS.get(type_str.lower()) return r if r else type_str def emit_single_type(lines, type_str, indent): type_str = resolve_type_str(type_str) # boolean if type_str == 'boolean': lines.append(f'{indent}xs:boolean') return # string or string(N) m = re.match(r'^string(\((\d+)\))?$', type_str) if m: length = m.group(2) if m.group(2) else '0' lines.append(f'{indent}xs:string') lines.append(f'{indent}') lines.append(f'{indent}\t{length}') lines.append(f'{indent}\tVariable') lines.append(f'{indent}') return # decimal(D,F) or decimal(D,F,nonneg) m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) if m: digits = m.group(1) fraction = m.group(2) sign = 'Nonnegative' if m.group(3) else 'Any' lines.append(f'{indent}xs:decimal') lines.append(f'{indent}') lines.append(f'{indent}\t{digits}') lines.append(f'{indent}\t{fraction}') lines.append(f'{indent}\t{sign}') lines.append(f'{indent}') return # date / dateTime / time m = re.match(r'^(date|dateTime|time)$', type_str) if m: fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'} fractions = fractions_map[type_str] lines.append(f'{indent}xs:dateTime') lines.append(f'{indent}') lines.append(f'{indent}\t{fractions}') lines.append(f'{indent}') return # V8 types if type_str in V8_TYPES: lines.append(f'{indent}{V8_TYPES[type_str]}') return # UI types if type_str in UI_TYPES: lines.append(f'{indent}{UI_TYPES[type_str]}') return # DCS types if type_str.startswith('DataComposition'): if type_str in DCS_MAP: lines.append(f'{indent}{DCS_MAP[type_str]}') return # DynamicList if type_str == 'DynamicList': lines.append(f'{indent}cfg:DynamicList') return # cfg: references if CFG_REF_PATTERN.match(type_str): lines.append(f'{indent}cfg:{type_str}') return # Fallback if '.' in type_str: lines.append(f'{indent}cfg:{type_str}') else: lines.append(f'{indent}{type_str}') def emit_type(lines, type_str, indent): if not type_str: lines.append(f'{indent}') return type_string = str(type_str) parts = [p.strip() for p in re.split(r'[|+]', type_string)] lines.append(f'{indent}') for part in parts: emit_single_type(lines, part, f'{indent}\t') lines.append(f'{indent}') # --- Element emitters --- def emit_element(lines, el, indent): type_key = None for key in TYPE_KEYS: if el.get(key) is not None: type_key = key break if not type_key: print("WARNING: Unknown element type, skipping", file=sys.stderr) return # Validate known keys for p_name in el.keys(): if p_name not in KNOWN_KEYS: print(f"WARNING: Element '{el.get(type_key, '')}': unknown key '{p_name}' -- ignored. Check SKILL.md for valid keys.", file=sys.stderr) name = get_element_name(el, type_key) eid = new_id() emitters = { 'group': emit_group, 'input': emit_input, 'check': emit_check, 'label': emit_label, 'labelField': emit_label_field, 'table': emit_table, 'pages': emit_pages, 'page': emit_page, 'button': emit_button, 'picture': emit_picture_decoration, 'picField': emit_picture_field, 'calendar': emit_calendar, 'cmdBar': emit_command_bar, 'popup': emit_popup, } emitter = emitters.get(type_key) if emitter: emitter(lines, el, name, eid, indent) def emit_group(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) # Group orientation group_val = str(el.get('group', '')) orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'alwaysHorizontal': 'AlwaysHorizontal', 'alwaysVertical': 'AlwaysVertical', } orientation = orientation_map.get(group_val) if orientation: lines.append(f'{inner}{orientation}') # Behavior if group_val == 'collapsible': lines.append(f'{inner}Vertical') lines.append(f'{inner}Collapsible') # Representation if el.get('representation'): repr_map = { 'none': 'None', 'normal': 'NormalSeparation', 'weak': 'WeakSeparation', 'strong': 'StrongSeparation', } repr_val = repr_map.get(str(el['representation']), str(el['representation'])) lines.append(f'{inner}{repr_val}') # ShowTitle if el.get('showTitle') is False: lines.append(f'{inner}false') # United if el.get('united') is False: lines.append(f'{inner}false') emit_common_flags(lines, el, inner) # Companion: ExtendedTooltip emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_input(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('titleLocation'): loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'} loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) lines.append(f'{inner}{loc}') if el.get('multiLine') is True: lines.append(f'{inner}true') if el.get('passwordMode') is True: lines.append(f'{inner}true') if el.get('choiceButton') is False: lines.append(f'{inner}false') if el.get('clearButton') is True: lines.append(f'{inner}true') if el.get('spinButton') is True: lines.append(f'{inner}true') if el.get('dropListButton') is True: lines.append(f'{inner}true') if el.get('markIncomplete') is True: lines.append(f'{inner}true') if el.get('skipOnInput') is True: lines.append(f'{inner}true') if el.get('autoMaxWidth') is False: lines.append(f'{inner}false') if el.get('autoMaxHeight') is False: lines.append(f'{inner}false') if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') if el.get('horizontalStretch') is True: lines.append(f'{inner}true') if el.get('verticalStretch') is True: lines.append(f'{inner}true') if el.get('inputHint'): emit_mltext(lines, inner, 'InputHint', str(el['inputHint'])) # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'input') lines.append(f'{indent}') def emit_check(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('titleLocation'): lines.append(f'{inner}{el["titleLocation"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'check') lines.append(f'{indent}') def emit_label(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('title'): formatted = 'true' if el.get('hyperlink') is True else 'false' lines.append(f'{inner}') lines.append(f'{inner}\t<v8:item>') lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>') lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(el["title"]))}</v8:content>') lines.append(f'{inner}\t</v8:item>') lines.append(f'{inner}') emit_common_flags(lines, el, inner) if el.get('hyperlink') is True: lines.append(f'{inner}true') if el.get('autoMaxWidth') is False: lines.append(f'{inner}false') if el.get('autoMaxHeight') is False: lines.append(f'{inner}false') if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'label') lines.append(f'{indent}') def emit_label_field(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('hyperlink') is True: lines.append(f'{inner}true') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'labelField') lines.append(f'{indent}') def emit_table(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('representation'): lines.append(f'{inner}{el["representation"]}') if el.get('changeRowSet') is True: lines.append(f'{inner}true') if el.get('changeRowOrder') is True: lines.append(f'{inner}true') if el.get('height'): lines.append(f'{inner}{el["height"]}') if el.get('header') is False: lines.append(f'{inner}
false
') if el.get('footer') is True: lines.append(f'{inner}') if el.get('commandBarLocation'): lines.append(f'{inner}{el["commandBarLocation"]}') if el.get('searchStringLocation'): lines.append(f'{inner}{el["searchStringLocation"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner) emit_companion(lines, 'SearchStringAddition', f'{name}\u0421\u0442\u0440\u043e\u043a\u0430\u041f\u043e\u0438\u0441\u043a\u0430', inner) emit_companion(lines, 'ViewStatusAddition', f'{name}\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430', inner) emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner) # Columns if el.get('columns') and len(el['columns']) > 0: lines.append(f'{inner}') for col in el['columns']: emit_element(lines, col, f'{inner}\t') lines.append(f'{inner}') emit_events(lines, el, name, inner, 'table') lines.append(f'{indent}
') def emit_pages(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('pagesRepresentation'): lines.append(f'{inner}{el["pagesRepresentation"]}') emit_common_flags(lines, el, inner) # Companion emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'pages') # Children (pages) if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_page(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('group'): orientation_map = { 'horizontal': 'Horizontal', 'vertical': 'Vertical', 'alwaysHorizontal': 'AlwaysHorizontal', 'alwaysVertical': 'AlwaysVertical', } orientation = orientation_map.get(str(el['group'])) if orientation: lines.append(f'{inner}{orientation}') # Companion emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_button(lines, el, name, eid, indent): lines.append(f'{indent}') def emit_picture_decoration(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('picture') or el.get('src'): ref = str(el.get('src') or el.get('picture')) lines.append(f'{inner}') lines.append(f'{inner}\t{ref}') lines.append(f'{inner}\ttrue') lines.append(f'{inner}') if el.get('hyperlink') is True: lines.append(f'{inner}true') if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'picture') lines.append(f'{indent}') def emit_picture_field(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('width'): lines.append(f'{inner}{el["width"]}') if el.get('height'): lines.append(f'{inner}{el["height"]}') # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'picField') lines.append(f'{indent}') def emit_calendar(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('path'): lines.append(f'{inner}{el["path"]}') emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) # Companions emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner) emit_companion(lines, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner) emit_events(lines, el, name, inner, 'calendar') lines.append(f'{indent}') def emit_command_bar(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' if el.get('autofill') is True: lines.append(f'{inner}true') emit_common_flags(lines, el, inner) # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') def emit_popup(lines, el, name, eid, indent): lines.append(f'{indent}') inner = f'{indent}\t' emit_title(lines, el, name, inner) emit_common_flags(lines, el, inner) if el.get('picture'): lines.append(f'{inner}') lines.append(f'{inner}\t{el["picture"]}') lines.append(f'{inner}\ttrue') lines.append(f'{inner}') if el.get('representation'): lines.append(f'{inner}{el["representation"]}') # Children if el.get('children') and len(el['children']) > 0: lines.append(f'{inner}') for child in el['children']: emit_element(lines, child, f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}') # --- Attribute emitter --- def emit_attributes(lines, attrs, indent): if not attrs or len(attrs) == 0: return lines.append(f'{indent}') for attr in attrs: attr_id = new_id() attr_name = str(attr['name']) lines.append(f'{indent}\t') inner = f'{indent}\t\t' if attr.get('title'): emit_mltext(lines, inner, 'Title', str(attr['title'])) # Type if attr.get('type'): emit_type(lines, str(attr['type']), inner) else: lines.append(f'{inner}') if attr.get('main') is True: lines.append(f'{inner}true') if attr.get('savedData') is True: lines.append(f'{inner}true') if attr.get('fillChecking'): lines.append(f'{inner}{attr["fillChecking"]}') # Columns (for ValueTable/ValueTree) if attr.get('columns') and len(attr['columns']) > 0: lines.append(f'{inner}') for col in attr['columns']: col_id = new_id() lines.append(f'{inner}\t') if col.get('title'): emit_mltext(lines, f'{inner}\t\t', 'Title', str(col['title'])) emit_type(lines, str(col.get('type', '')), f'{inner}\t\t') lines.append(f'{inner}\t') lines.append(f'{inner}') lines.append(f'{indent}\t') lines.append(f'{indent}') # --- Parameter emitter --- def emit_parameters(lines, params, indent): if not params or len(params) == 0: return lines.append(f'{indent}') for param in params: lines.append(f'{indent}\t') inner = f'{indent}\t\t' emit_type(lines, str(param.get('type', '')), inner) if param.get('key') is True: lines.append(f'{inner}true') lines.append(f'{indent}\t') lines.append(f'{indent}') # --- Command emitter --- def emit_commands(lines, cmds, indent): if not cmds or len(cmds) == 0: return lines.append(f'{indent}') for cmd in cmds: cmd_id = new_id() lines.append(f'{indent}\t') inner = f'{indent}\t\t' if cmd.get('title'): emit_mltext(lines, inner, 'Title', str(cmd['title'])) if cmd.get('action'): lines.append(f'{inner}{cmd["action"]}') if cmd.get('shortcut'): lines.append(f'{inner}{cmd["shortcut"]}') if cmd.get('picture'): lines.append(f'{inner}') lines.append(f'{inner}\t{cmd["picture"]}') lines.append(f'{inner}\ttrue') lines.append(f'{inner}') if cmd.get('representation'): lines.append(f'{inner}{cmd["representation"]}') lines.append(f'{indent}\t') lines.append(f'{indent}') # --- Properties emitter --- PROP_MAP = { "autoTitle": "AutoTitle", "windowOpeningMode": "WindowOpeningMode", "commandBarLocation": "CommandBarLocation", "saveDataInSettings": "SaveDataInSettings", "autoSaveDataInSettings": "AutoSaveDataInSettings", "autoTime": "AutoTime", "usePostingMode": "UsePostingMode", "repostOnWrite": "RepostOnWrite", "autoURL": "AutoURL", "autoFillCheck": "AutoFillCheck", "customizable": "Customizable", "enterKeyBehavior": "EnterKeyBehavior", "verticalScroll": "VerticalScroll", "scalingMode": "ScalingMode", "useForFoldersAndItems": "UseForFoldersAndItems", "reportResult": "ReportResult", "detailsData": "DetailsData", "reportFormType": "ReportFormType", "autoShowState": "AutoShowState", "width": "Width", "height": "Height", "group": "Group", } def emit_properties(lines, props, indent): if not props: return for p_name, p_value in props.items(): xml_name = PROP_MAP.get(p_name) if not xml_name: # Auto PascalCase xml_name = p_name[0].upper() + p_name[1:] # Convert boolean to lowercase if isinstance(p_value, bool): val = 'true' if p_value else 'false' else: val = str(p_value) lines.append(f'{indent}<{xml_name}>{val}') def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") global _next_id parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON', allow_abbrev=False) parser.add_argument('-JsonPath', type=str, required=True) parser.add_argument('-OutputPath', type=str, required=True) args = parser.parse_args() # --- 1. Load and validate JSON --- json_path = args.JsonPath if not os.path.exists(json_path): print(f"File not found: {json_path}", file=sys.stderr) sys.exit(1) with open(json_path, 'r', encoding='utf-8-sig') as f: defn = json.load(f) # --- 2. Main compilation --- _next_id = 0 lines = [] lines.append('') lines.append('
') # Title form_title = defn.get('title') if not form_title and defn.get('properties') and defn['properties'].get('title'): form_title = defn['properties']['title'] if form_title: emit_mltext(lines, '\t', 'Title', str(form_title)) # Properties (skip 'title' — handled above) if defn.get('properties'): props_clone = {k: v for k, v in defn['properties'].items() if k != 'title'} emit_properties(lines, props_clone, '\t') # CommandSet (excluded commands) if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: lines.append('\t') for cmd in defn['excludedCommands']: lines.append(f'\t\t{cmd}') lines.append('\t') # AutoCommandBar (always present, id=-1) lines.append('\t') lines.append('\t\tRight') lines.append('\t\tfalse') lines.append('\t') # Events if defn.get('events'): for evt_name in defn['events']: if evt_name not in KNOWN_FORM_EVENTS: print(f"[WARN] Unknown form event '{evt_name}'. Known: {', '.join(KNOWN_FORM_EVENTS)}") lines.append('\t') for evt_name, evt_handler in defn['events'].items(): lines.append(f'\t\t{evt_handler}') lines.append('\t') # ChildItems (elements) if defn.get('elements') and len(defn['elements']) > 0: lines.append('\t') for el in defn['elements']: emit_element(lines, el, '\t\t') lines.append('\t') # Attributes emit_attributes(lines, defn.get('attributes'), '\t') # Parameters emit_parameters(lines, defn.get('parameters'), '\t') # Commands emit_commands(lines, defn.get('commands'), '\t') # Close lines.append('
') # --- 3. Write output --- out_path = args.OutputPath if not os.path.isabs(out_path): out_path = os.path.join(os.getcwd(), out_path) out_dir = os.path.dirname(out_path) if out_dir and not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) content = '\n'.join(lines) + '\n' write_utf8_bom(out_path, content) # --- 4. Auto-register form in parent object XML --- # Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml form_xml_dir = os.path.dirname(out_path) # Ext form_name_dir = os.path.dirname(form_xml_dir) # FormName forms_dir = os.path.dirname(form_name_dir) # Forms object_dir = os.path.dirname(forms_dir) # ObjectName type_plural_dir = os.path.dirname(object_dir) # TypePlural form_name = os.path.basename(form_name_dir) object_name = os.path.basename(object_dir) forms_leaf = os.path.basename(forms_dir) if forms_leaf == 'Forms': object_xml_path = os.path.join(type_plural_dir, f'{object_name}.xml') if os.path.exists(object_xml_path): with open(object_xml_path, 'r', encoding='utf-8-sig') as f: raw_text = f.read() # Check if already registered if f'
{form_name}
' not in raw_text: # Insert before if '' in raw_text: insert_line = f'\t\t\t
{form_name}
\n' raw_text = raw_text.replace('', insert_line + '\t\t', 1) elif '' in raw_text: replacement = f'\n\t\t\t
{form_name}
\n\t\t
' raw_text = raw_text.replace('', replacement, 1) write_utf8_bom(object_xml_path, raw_text) print(f" Registered:
{form_name}
in {object_name}.xml") # --- 5. Summary --- el_count = _next_id print(f"[OK] Compiled: {args.OutputPath}") print(f" Elements+IDs: {el_count}") if defn.get('attributes'): print(f" Attributes: {len(defn['attributes'])}") if defn.get('commands'): print(f" Commands: {len(defn['commands'])}") if defn.get('parameters'): print(f" Parameters: {len(defn['parameters'])}") if __name__ == '__main__': main()