feat(form-compile): add --from-object mode for auto form generation from metadata

Read 1C object XML (Document/Catalog), apply ERP preset, generate Form.xml
automatically. Supports Item/List/Choice/Folder purposes with auto-resolve
of object path and purpose from OutputPath convention. Also extends DSL
with DynamicList Settings, Table choiceMode/initialTreeView/enableDrag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-04-12 18:03:34 +03:00
co-authored by Claude Opus 4.6
parent 3c48451704
commit 076bbcb9d5
4 changed files with 3092 additions and 1184 deletions
+26 -16
View File
@@ -1,7 +1,7 @@
--- ---
name: form-compile name: form-compile
description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму
argument-hint: <JsonPath> <OutputPath> argument-hint: <JsonPath> <OutputPath> | -FromObject <OutputPath>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
@@ -9,29 +9,30 @@ allowed-tools:
- Glob - Glob
--- ---
# /form-compile — Генерация Form.xml из JSON DSL # /form-compile — Генерация Form.xml
Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID. Два режима:
1. **JSON DSL** — из JSON-определения формы
2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно. > **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно.
## Использование
```
/form-compile <JsonPath> <OutputPath>
```
## Параметры ## Параметры
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|------------|:------------:|-----------------------------------| |------------|:------------:|---------------------------------|
| JsonPath | да | Путь к JSON-определению формы | | JsonPath | режим 1 | Путь к JSON-определению формы |
| OutputPath | да | Путь к выходному файлу Form.xml | | OutputPath | да | Путь к выходному Form.xml |
| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>" # Режим JSON DSL
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
``` ```
## JSON DSL — справка ## JSON DSL — справка
@@ -167,6 +168,12 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `footer: true` | Показать подвал | | `footer: true` | Показать подвал |
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` | | `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` | | `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
| `choiceMode: true` | Режим выбора (для форм выбора) |
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
| `enableDrag: true` | Разрешить перетаскивание |
| `enableStartDrag: true` | Разрешить начало перетаскивания |
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
### Страницы (pages + page) ### Страницы (pages + page)
@@ -221,6 +228,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```json ```json
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true } { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
{ "name": "Список", "type": "DynamicList", "main": true, "settings": {
"mainTable": "Catalog.Номенклатура", "dynamicDataRead": true
}}
{ "name": "Итого", "type": "decimal(15,2)" } { "name": "Итого", "type": "decimal(15,2)" }
{ "name": "Таблица", "type": "ValueTable", "columns": [ { "name": "Таблица", "type": "ValueTable", "columns": [
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
@@ -0,0 +1,44 @@
{
"name": "erp-standard",
"description": "ERP 8.3.24 standard form layout",
"document.item": {
"header": {
"position": "insidePage",
"layout": "2col",
"distribute": "even",
"dateTitle": "от"
},
"footer": {
"fields": ["Комментарий"],
"position": "insidePage"
},
"tabularSections": {
"container": "pages",
"exclude": ["ДополнительныеРеквизиты"],
"lineNumber": true
},
"additional": {
"position": "page",
"layout": "2col",
"bspGroup": true
},
"properties": {
"autoTitle": false
}
},
"catalog.item": {
"codeDescription": {
"layout": "horizontal",
"order": "descriptionFirst"
},
"parent": {
"title": "Входит в группу",
"position": "afterCodeDescription"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "Представления"]
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,764 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-compile v1.4 — Compile 1C managed form from JSON # form-compile v1.5 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import copy
import json import json
import os import os
import re import re
import sys import sys
import uuid import uuid
import xml.etree.ElementTree as ET
from collections import OrderedDict
# ═══════════════════════════════════════════════════════════════════════════
# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation
# ═══════════════════════════════════════════════════════════════════════════
NS = {
'md': 'http://v8.1c.ru/8.3/MDClasses',
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
'v8': 'http://v8.1c.ru/8.1/data/core',
}
def _et_find(node, path):
"""Find with namespace map."""
return node.find(path, NS)
def _et_findall(node, path):
"""Findall with namespace map."""
return node.findall(path, NS)
def _et_text(node, path, default=''):
"""Get text of a sub-element, or default."""
el = node.find(path, NS)
return el.text if el is not None and el.text else default
def parse_object_meta(object_path):
"""Parse 1C metadata XML and return dict with Type, Name, Synonym, Attributes, TabularSections, etc."""
tree = ET.parse(object_path)
root = tree.getroot()
# Detect object type from root child
meta_root = _et_find(root, '.')
# Root is MetaDataObject; first child is the type node
type_node = None
for child in root:
type_node = child
break
if type_node is None:
print("Not a 1C metadata XML: " + object_path, file=sys.stderr)
sys.exit(1)
# Extract local name (strip namespace)
obj_type = type_node.tag.split('}')[-1] if '}' in type_node.tag else type_node.tag
props_node = _et_find(type_node, 'md:Properties')
child_objs = _et_find(type_node, 'md:ChildObjects')
# Name
obj_name = _et_text(props_node, 'md:Name')
# Synonym (Russian)
synonym = obj_name
syn_node = _et_find(props_node, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
if syn_node is not None and syn_node.text:
synonym = syn_node.text
def extract_type(type_parent):
"""Extract type string from md:Type element."""
if type_parent is None:
return 'string'
types = []
for t in _et_findall(type_parent, 'v8:Type'):
if t.text:
types.append(t.text)
if not types:
return 'string'
return ' | '.join(types)
def is_ref_type(t):
return bool(re.search(r'Ref\.', t) or re.search(r'\u0441\u0441\u044b\u043b\u043a\u0430\.', t))
def extract_attrs(parent_node):
"""Extract attribute list from ChildObjects."""
result = []
if parent_node is None:
return result
for attr_node in _et_findall(parent_node, 'md:Attribute'):
ap = _et_find(attr_node, 'md:Properties')
a_name = _et_text(ap, 'md:Name')
a_syn_node = _et_find(ap, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
a_syn = a_syn_node.text if a_syn_node is not None and a_syn_node.text else a_name
a_type_node = _et_find(ap, 'md:Type')
a_type = extract_type(a_type_node)
result.append({
'Name': a_name,
'Synonym': a_syn,
'Type': a_type,
'IsRef': is_ref_type(a_type),
})
return result
# Attributes
attributes = extract_attrs(child_objs)
# Tabular sections
tabular_sections = []
if child_objs is not None:
for ts_node in _et_findall(child_objs, 'md:TabularSection'):
tsp = _et_find(ts_node, 'md:Properties')
ts_name = _et_text(tsp, 'md:Name')
ts_syn_node = _et_find(tsp, "md:Synonym/v8:item[v8:lang='ru']/v8:content")
ts_syn = ts_syn_node.text if ts_syn_node is not None and ts_syn_node.text else ts_name
ts_co = _et_find(ts_node, 'md:ChildObjects')
ts_cols = extract_attrs(ts_co)
tabular_sections.append({
'Name': ts_name,
'Synonym': ts_syn,
'Columns': ts_cols,
})
meta = {
'Type': obj_type,
'Name': obj_name,
'Synonym': synonym,
'Attributes': attributes,
'TabularSections': tabular_sections,
}
# Type-specific properties
if obj_type == 'Document':
nt_node = _et_find(props_node, 'md:NumberType')
meta['NumberType'] = nt_node.text if nt_node is not None and nt_node.text else 'String'
elif obj_type == 'Catalog':
cl_node = _et_find(props_node, 'md:CodeLength')
meta['CodeLength'] = int(cl_node.text) if cl_node is not None and cl_node.text else 0
dl_node = _et_find(props_node, 'md:DescriptionLength')
meta['DescriptionLength'] = int(dl_node.text) if dl_node is not None and dl_node.text else 0
hi_node = _et_find(props_node, 'md:Hierarchical')
meta['Hierarchical'] = (hi_node is not None and hi_node.text == 'true')
ht_node = _et_find(props_node, 'md:HierarchyType')
meta['HierarchyType'] = ht_node.text if ht_node is not None and ht_node.text else 'HierarchyFoldersAndItems'
owners = []
for ow in _et_findall(props_node, 'md:Owners/xr:Item'):
if ow.text:
owners.append(ow.text)
meta['Owners'] = owners
return meta
def _deep_merge(base, overlay):
"""Deep merge two dicts. overlay wins on conflicts."""
if not overlay:
return base
if not base:
return overlay
result = {}
for k in base:
result[k] = base[k]
for k in overlay:
if k in result and isinstance(result[k], dict) and isinstance(overlay[k], dict):
result[k] = _deep_merge(result[k], overlay[k])
else:
result[k] = overlay[k]
return result
def load_preset(preset_name, script_dir, out_path_resolved):
"""Load preset: hardcoded defaults -> built-in JSON -> project-level JSON, with deep merge."""
defaults = {
'document.item': {
'header': {'position': 'insidePage', 'layout': '2col', 'distribute': 'even', 'dateTitle': '\u043e\u0442'},
'footer': {'fields': ['\u041a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439'], 'position': 'insidePage'},
'tabularSections': {'container': 'pages', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b'], 'lineNumber': True},
'additional': {'position': 'page', 'layout': '2col', 'bspGroup': True},
'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
'commandBar': 'auto',
'properties': {'autoTitle': False},
},
'document.list': {
'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True,
'tableCommandBar': 'none', 'commandBar': 'auto',
'properties': {},
},
'document.choice': {
'basedOn': 'document.list',
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
'catalog.item': {
'header': {'layout': '1col', 'distribute': 'left'},
'codeDescription': {'layout': 'horizontal', 'order': 'descriptionFirst'},
'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443', 'position': 'afterCodeDescription'},
'owner': {'readOnly': True, 'position': 'first'},
'tabularSections': {'container': 'inline', 'exclude': ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f'], 'lineNumber': True},
'footer': {'fields': [], 'position': 'none'},
'additional': {'position': 'none', 'bspGroup': True},
'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}},
'commandBar': 'auto',
'properties': {},
},
'catalog.folder': {
'parent': {'title': '\u0412\u0445\u043e\u0434\u0438\u0442 \u0432 \u0433\u0440\u0443\u043f\u043f\u0443'},
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
'catalog.list': {
'columns': 'all', 'columnType': 'labelField', 'hiddenRef': True,
'tableCommandBar': 'none', 'commandBar': 'auto',
'properties': {},
},
'catalog.choice': {
'basedOn': 'catalog.list', 'choiceMode': True,
'properties': {'windowOpeningMode': 'LockOwnerWindow'},
},
}
# Try built-in preset
preset_dir = os.path.join(os.path.dirname(script_dir), 'presets')
built_in_path = os.path.join(preset_dir, f'{preset_name}.json')
if os.path.isfile(built_in_path):
with open(built_in_path, 'r', encoding='utf-8-sig') as f:
preset_data = json.load(f)
for k in list(preset_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
# Try project-level preset (scan up from output path)
scan_dir = os.path.dirname(out_path_resolved)
while scan_dir:
proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json')
if os.path.isfile(proj_preset):
with open(proj_preset, 'r', encoding='utf-8-sig') as f:
proj_data = json.load(f)
for k in list(proj_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), proj_data[k])
break
parent_dir = os.path.dirname(scan_dir)
if parent_dir == scan_dir:
break
scan_dir = parent_dir
# Resolve basedOn references
for k in list(defaults.keys()):
sect = defaults[k]
if isinstance(sect, dict) and 'basedOn' in sect:
base_name = sect['basedOn']
if base_name in defaults:
merged = _deep_merge(defaults[base_name], sect)
merged.pop('basedOn', None)
defaults[k] = merged
return defaults
def new_field_element(attr_name, data_path, attr_type, field_defaults, extra_props=None):
"""Build a field element DSL entry."""
is_ref = bool(re.search(r'Ref\.', attr_type))
is_bool = bool(re.match(r'^\s*xs:boolean\s*$', attr_type) or attr_type == 'boolean' or re.search(r'Boolean', attr_type))
el_type = 'input'
if is_bool and field_defaults and field_defaults.get('boolean') and field_defaults['boolean'].get('element') == 'check':
el_type = 'check'
el = OrderedDict()
el[el_type] = attr_name
el['path'] = data_path
# Apply ref defaults
if is_ref and field_defaults and field_defaults.get('ref'):
if field_defaults['ref'].get('choiceButton') is True:
el['choiceButton'] = True
# Extra props
if extra_props:
for k in extra_props:
el[k] = extra_props[k]
return el
# --- Catalog DSL generators ---
def generate_catalog_dsl(meta, preset_data, purpose):
purpose_key = f"catalog.{purpose.lower()}"
p = preset_data.get(purpose_key, {})
fd = p.get('fieldDefaults', {})
dispatch = {
'Folder': lambda: generate_catalog_folder_dsl(meta, p),
'List': lambda: generate_catalog_list_dsl(meta, p),
'Choice': lambda: generate_catalog_choice_dsl(meta, p, preset_data),
'Item': lambda: generate_catalog_item_dsl(meta, p, fd),
}
return dispatch[purpose]()
def generate_catalog_folder_dsl(meta, p):
elements = []
# Code (if CodeLength > 0)
if meta.get('CodeLength', 0) > 0:
elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
# Description
elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
# Parent
parent_title = p.get('parent', {}).get('title')
parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')])
if parent_title:
parent_el['title'] = parent_title
elements.append(parent_el)
props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')])
if p.get('properties'):
for k in p['properties']:
props[k] = p['properties'][k]
form_props = OrderedDict([('useForFoldersAndItems', 'Folders')])
for k in props:
form_props[k] = props[k]
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', elements),
('attributes', [
OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)])
]),
])
def generate_catalog_list_dsl(meta, p):
columns = []
# Description always first
columns.append(OrderedDict([('labelField', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Description')]))
# Code if present
if meta.get('CodeLength', 0) > 0:
columns.append(OrderedDict([('labelField', '\u041a\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Code')]))
# Custom attributes
for attr in meta['Attributes']:
columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
# Hidden ref
if p.get('hiddenRef', True) is not False:
columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('visible', False)]))
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns),
])
# Hierarchical properties
if meta.get('Hierarchical'):
table_el['initialTreeView'] = 'ExpandTopLevel'
table_el['enableStartDrag'] = True
table_el['enableDrag'] = True
form_props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
form_props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', [table_el]),
('attributes', [
OrderedDict([
('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True),
('settings', OrderedDict([('mainTable', f"Catalog.{meta['Name']}"), ('dynamicDataRead', True)])),
])
]),
])
def generate_catalog_choice_dsl(meta, p, preset_data):
# Start from list
list_key = 'catalog.list'
lp = preset_data.get(list_key, {})
dsl = generate_catalog_list_dsl(meta, lp)
# Add choice-specific properties
dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow'
if p.get('properties'):
for k in p['properties']:
dsl['properties'][k] = p['properties'][k]
# Set ChoiceMode on table
dsl['elements'][0]['choiceMode'] = True
return dsl
def generate_catalog_item_dsl(meta, p, fd):
header_children = []
# Owner (if subordinate)
if meta.get('Owners') and len(meta['Owners']) > 0:
owner_el = OrderedDict([('input', '\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Owner'), ('readOnly', True)])
header_children.append(owner_el)
# Code + Description
cd_layout = (p.get('codeDescription') or {}).get('layout', 'horizontal')
cd_order = (p.get('codeDescription') or {}).get('order', 'descriptionFirst')
has_code = meta.get('CodeLength', 0) > 0
if cd_layout == 'horizontal' and has_code:
cd_children = []
desc_el = OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])
code_el = OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])
if cd_order == 'descriptionFirst':
cd_children = [desc_el, code_el]
else:
cd_children = [code_el, desc_el]
header_children.append(OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('showTitle', False),
('representation', 'none'), ('children', cd_children),
]))
else:
# Vertical or no code
header_children.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')]))
if has_code:
header_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')]))
# Parent (for hierarchical catalogs)
parent_pos = (p.get('parent') or {}).get('position', 'afterCodeDescription')
parent_title = (p.get('parent') or {}).get('title')
if meta.get('Hierarchical'):
parent_el = OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent')])
if parent_title:
parent_el['title'] = parent_title
if parent_pos == 'beforeCodeDescription':
insert_idx = 1 if (meta.get('Owners') and len(meta['Owners']) > 0) else 0
header_children.insert(insert_idx, parent_el)
else:
# afterCodeDescription (default)
header_children.append(parent_el)
# Custom attributes -> header
footer_field_names = (p.get('footer') or {}).get('fields', [])
for attr in meta['Attributes']:
if attr['Name'] in footer_field_names:
continue
header_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
# Build root elements
root_elements = []
# ГруппаШапка
root_elements.append(OrderedDict([
('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False),
('representation', 'none'), ('children', header_children),
]))
# Tabular sections
ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b', '\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f']
if (p.get('tabularSections') or {}).get('exclude'):
ts_exclude = p['tabularSections']['exclude']
ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True)
visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude]
for ts in visible_ts:
ts_cols = []
if ts_line_number:
ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")]))
for col in ts['Columns']:
ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
root_elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)]))
# Footer fields
for fn in footer_field_names:
f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None)
if f_attr:
root_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd))
# BSP group
bsp_group = (p.get('additional') or {}).get('bspGroup', True)
if bsp_group:
root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
# Properties
form_props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
form_props[k] = p['properties'][k]
# UseForFoldersAndItems
if meta.get('Hierarchical') and meta.get('HierarchyType') == 'HierarchyFoldersAndItems':
form_props['useForFoldersAndItems'] = 'Items'
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', root_elements),
('attributes', [
OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"CatalogObject.{meta['Name']}"), ('main', True)])
]),
])
# --- Document DSL generators ---
def generate_document_dsl(meta, preset_data, purpose):
purpose_key = f"document.{purpose.lower()}"
p = preset_data.get(purpose_key, {})
fd = p.get('fieldDefaults', {})
dispatch = {
'List': lambda: generate_document_list_dsl(meta, p),
'Choice': lambda: generate_document_choice_dsl(meta, p, preset_data),
'Item': lambda: generate_document_item_dsl(meta, p, fd),
}
return dispatch[purpose]()
def generate_document_list_dsl(meta, p):
columns = []
# All custom attributes as labelField
for attr in meta['Attributes']:
columns.append(OrderedDict([('labelField', attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")]))
# Hidden ref
if p.get('hiddenRef', True):
columns.append(OrderedDict([('labelField', '\u0421\u0441\u044b\u043b\u043a\u0430'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Ref'), ('visible', False)]))
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns),
])
form_props = OrderedDict()
if p.get('properties'):
for k in p['properties']:
form_props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', [table_el]),
('attributes', [
OrderedDict([
('name', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('type', 'DynamicList'), ('main', True),
('settings', OrderedDict([('mainTable', f"Document.{meta['Name']}"), ('dynamicDataRead', True)])),
])
]),
])
def generate_document_choice_dsl(meta, p, preset_data):
list_key = 'document.list'
lp = preset_data.get(list_key, {})
dsl = generate_document_list_dsl(meta, lp)
dsl['properties']['windowOpeningMode'] = 'LockOwnerWindow'
if p.get('properties'):
for k in p['properties']:
dsl['properties'][k] = p['properties'][k]
return dsl
def generate_document_item_dsl(meta, p, fd):
header_pos = (p.get('header') or {}).get('position', 'insidePage')
header_layout = (p.get('header') or {}).get('layout', '2col')
header_distribute = (p.get('header') or {}).get('distribute', 'even')
date_title = (p.get('header') or {}).get('dateTitle', '\u043e\u0442')
footer_fields = (p.get('footer') or {}).get('fields', [])
footer_pos = (p.get('footer') or {}).get('position', 'insidePage')
add_pos = (p.get('additional') or {}).get('position', 'page')
add_layout = (p.get('additional') or {}).get('layout', '2col')
add_bsp_group = (p.get('additional') or {}).get('bspGroup', True)
add_left = (p.get('additional') or {}).get('left', [])
add_right = (p.get('additional') or {}).get('right', [])
header_right = (p.get('header') or {}).get('right', [])
ts_exclude = ['\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b']
if (p.get('tabularSections') or {}).get('exclude'):
ts_exclude = p['tabularSections']['exclude']
ts_line_number = (p.get('tabularSections') or {}).get('lineNumber', True)
# Classify attributes
claimed = {}
for fn in footer_fields:
claimed[fn] = 'footer'
for fn in header_right:
claimed[fn] = 'header.right'
for fn in add_left:
claimed[fn] = 'additional.left'
for fn in add_right:
claimed[fn] = 'additional.right'
unclaimed = [attr for attr in meta['Attributes'] if attr['Name'] not in claimed]
# Distribute unclaimed
left_attrs = []
right_extra_attrs = []
if header_distribute == 'left':
left_attrs = unclaimed
elif header_distribute == 'right':
right_extra_attrs = unclaimed
else: # "even"
import math
half = math.ceil(len(unclaimed) / 2) if unclaimed else 0
left_attrs = unclaimed[:half]
right_extra_attrs = unclaimed[half:]
# Build ГруппаНомерДата
num_date_children = [
OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Number'), ('autoMaxWidth', False), ('width', 9)]),
OrderedDict([('input', '\u0414\u0430\u0442\u0430'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Date'), ('title', date_title)]),
]
num_date_group = OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041d\u043e\u043c\u0435\u0440\u0414\u0430\u0442\u0430'), ('showTitle', False), ('children', num_date_children),
])
# Build left column
left_children = [num_date_group]
for attr in left_attrs:
left_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
# Build right column
right_children = []
for rn in header_right:
r_attr = next((a for a in meta['Attributes'] if a['Name'] == rn), None)
if r_attr:
right_children.append(new_field_element(r_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{r_attr['Name']}", r_attr['Type'], fd))
for attr in right_extra_attrs:
right_children.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd))
# Header group
if header_layout == '2col' and len(right_children) > 0:
header_group = OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'),
('children', [
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', left_children)]),
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', right_children)]),
]),
])
else:
# 1col or no right items
all_header_fields = left_children + right_children
header_group = OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430'), ('showTitle', False), ('representation', 'none'),
('children', [
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0428\u0430\u043f\u043a\u0430\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', all_header_fields)]),
]),
])
# Footer elements
footer_elements = []
for fn in footer_fields:
f_attr = next((a for a in meta['Attributes'] if a['Name'] == fn), None)
if f_attr:
footer_elements.append(new_field_element(f_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{f_attr['Name']}", f_attr['Type'], fd))
# Visible tabular sections
visible_ts = [ts for ts in meta['TabularSections'] if ts['Name'] not in ts_exclude]
# Additional page content
additional_page = None
if add_pos == 'page':
add_left_els = []
add_right_els = []
for aln in add_left:
al_attr = next((a for a in meta['Attributes'] if a['Name'] == aln), None)
if al_attr:
add_left_els.append(new_field_element(al_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{al_attr['Name']}", al_attr['Type'], fd))
for arn in add_right:
ar_attr = next((a for a in meta['Attributes'] if a['Name'] == arn), None)
if ar_attr:
add_right_els.append(new_field_element(ar_attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{ar_attr['Name']}", ar_attr['Type'], fd))
add_page_children = []
if add_layout == '2col':
add_page_children.append(OrderedDict([
('group', 'horizontal'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b'), ('showTitle', False),
('children', [
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041b\u0435\u0432\u043e'), ('showTitle', False), ('children', add_left_els)]),
OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b\u041f\u0440\u0430\u0432\u043e'), ('showTitle', False), ('children', add_right_els)]),
]),
]))
else:
add_page_children.extend(add_left_els + add_right_els)
if add_bsp_group:
add_page_children.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
additional_page = OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('title', '\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e'), ('children', add_page_children)])
# Build TS page elements
ts_pages = []
for ts in visible_ts:
ts_cols = []
if ts_line_number:
ts_cols.append(OrderedDict([('labelField', f"{ts['Name']}\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.LineNumber")]))
for col in ts['Columns']:
ts_cols.append(new_field_element(f"{ts['Name']}{col['Name']}", f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}.{col['Name']}", col['Type'], fd))
ts_pages.append(OrderedDict([
('page', f"\u0413\u0440\u0443\u043f\u043f\u0430{ts['Name']}"), ('title', ts['Synonym']),
('children', [
OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])
]),
]))
# Assemble root elements
root_elements = []
if len(visible_ts) == 0:
# Simple form - no Pages
root_elements.append(header_group)
if footer_elements:
root_elements.extend(footer_elements)
if add_bsp_group and add_pos != 'none':
root_elements.append(OrderedDict([('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u044b')]))
else:
# Pages form
if header_pos == 'abovePages':
root_elements.append(header_group)
pages_children = list(ts_pages)
if additional_page:
pages_children.append(additional_page)
root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)]))
else:
# insidePage (default)
osnovnoe_children = [header_group]
if footer_pos == 'insidePage' and footer_elements:
osnovnoe_children.extend(footer_elements)
pages_children = []
pages_children.append(OrderedDict([('page', '\u0413\u0440\u0443\u043f\u043f\u0430\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('title', '\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435'), ('children', osnovnoe_children)]))
pages_children.extend(ts_pages)
if additional_page:
pages_children.append(additional_page)
root_elements.append(OrderedDict([('pages', '\u0413\u0440\u0443\u043f\u043f\u0430\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b'), ('children', pages_children)]))
# Footer below pages
if footer_pos == 'belowPages' and footer_elements:
root_elements.extend(footer_elements)
# Properties
form_props = OrderedDict([('autoTitle', False)])
if p.get('properties'):
for k in p['properties']:
form_props[k] = p['properties'][k]
return OrderedDict([
('title', meta['Synonym']),
('properties', form_props),
('elements', root_elements),
('attributes', [
OrderedDict([('name', '\u041e\u0431\u044a\u0435\u043a\u0442'), ('type', f"DocumentObject.{meta['Name']}"), ('main', True)])
]),
])
# ═══════════════════════════════════════════════════════════════════════════
# END OF FROM-OBJECT MODE FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════
def esc_xml(s): def esc_xml(s):
@@ -114,6 +866,8 @@ KNOWN_KEYS = {
"type", "command", "stdCommand", "defaultButton", "locationInCommandBar", "type", "command", "stdCommand", "defaultButton", "locationInCommandBar",
"src", "src",
"autofill", "autofill",
"choiceMode", "initialTreeView", "enableDrag", "enableStartDrag",
"rowPictureDataPath", "tableAutofill",
} }
TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page", TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page",
@@ -615,9 +1369,29 @@ def emit_table(lines, el, name, eid, indent):
if el.get('searchStringLocation'): if el.get('searchStringLocation'):
lines.append(f'{inner}<SearchStringLocation>{el["searchStringLocation"]}</SearchStringLocation>') lines.append(f'{inner}<SearchStringLocation>{el["searchStringLocation"]}</SearchStringLocation>')
if el.get('choiceMode') is True:
lines.append(f'{inner}<ChoiceMode>true</ChoiceMode>')
if el.get('initialTreeView'):
lines.append(f'{inner}<InitialTreeView>{el["initialTreeView"]}</InitialTreeView>')
if el.get('enableStartDrag') is True:
lines.append(f'{inner}<EnableStartDrag>true</EnableStartDrag>')
if el.get('enableDrag') is True:
lines.append(f'{inner}<EnableDrag>true</EnableDrag>')
if el.get('rowPictureDataPath'):
lines.append(f'{inner}<RowPictureDataPath>{el["rowPictureDataPath"]}</RowPictureDataPath>')
# Companions # 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, '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) # AutoCommandBar — with optional Autofill control
if el.get('tableAutofill') is not None:
acb_id = new_id()
acb_name = f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
af_val = 'true' if el['tableAutofill'] else 'false'
lines.append(f'{inner}<AutoCommandBar name="{acb_name}" id="{acb_id}">')
lines.append(f'{inner}\t<Autofill>{af_val}</Autofill>')
lines.append(f'{inner}</AutoCommandBar>')
else:
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, '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, '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) emit_companion(lines, 'SearchControlAddition', f'{name}\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u041f\u043e\u0438\u0441\u043a\u043e\u043c', inner)
@@ -897,6 +1671,19 @@ def emit_attributes(lines, attrs, indent):
lines.append(f'{inner}\t</Column>') lines.append(f'{inner}\t</Column>')
lines.append(f'{inner}</Columns>') lines.append(f'{inner}</Columns>')
# Settings (for DynamicList)
if attr.get('settings'):
s = attr['settings']
lines.append(f'{inner}<Settings xsi:type="DynamicList">')
si = f'{inner}\t'
if s.get('mainTable'):
lines.append(f'{si}<MainTable>{s["mainTable"]}</MainTable>')
mq = 'true' if s.get('manualQuery') else 'false'
lines.append(f'{si}<ManualQuery>{mq}</ManualQuery>')
ddr = 'true' if s.get('dynamicDataRead') else 'false'
lines.append(f'{si}<DynamicDataRead>{ddr}</DynamicDataRead>')
lines.append(f'{inner}</Settings>')
lines.append(f'{indent}\t</Attribute>') lines.append(f'{indent}\t</Attribute>')
lines.append(f'{indent}</Attributes>') lines.append(f'{indent}</Attributes>')
@@ -1001,6 +1788,7 @@ def emit_properties(lines, props, indent):
lines.append(f'{indent}<{xml_name}>{val}</{xml_name}>') lines.append(f'{indent}<{xml_name}>{val}</{xml_name}>')
def detect_format_version(d): def detect_format_version(d):
while d: while d:
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
@@ -1022,23 +1810,130 @@ def main():
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
global _next_id global _next_id
parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON', allow_abbrev=False) parser = argparse.ArgumentParser(description='Compile 1C managed form from JSON or object metadata', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True) parser.add_argument('-JsonPath', type=str, default=None)
parser.add_argument('-OutputPath', type=str, required=True) parser.add_argument('-OutputPath', type=str, required=True)
parser.add_argument('-FromObject', action='store_true', default=False)
parser.add_argument('-ObjectPath', type=str, default=None)
parser.add_argument('-Purpose', type=str, default=None)
parser.add_argument('-Preset', type=str, default='erp-standard')
parser.add_argument('-EmitDsl', type=str, default=None)
args = parser.parse_args() args = parser.parse_args()
# Form name -> purpose mapping
_FORM_NAME_TO_PURPOSE = {
'\u0424\u043e\u0440\u043c\u0430\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаДокумента
'\u0424\u043e\u0440\u043c\u0430\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u0430': 'Item', # ФормаЭлемента
'\u0424\u043e\u0440\u043c\u0430\u0421\u043f\u0438\u0441\u043a\u0430': 'List', # ФормаСписка
'\u0424\u043e\u0440\u043c\u0430\u0412\u044b\u0431\u043e\u0440\u0430': 'Choice', # ФормаВыбора
'\u0424\u043e\u0440\u043c\u0430\u0413\u0440\u0443\u043f\u043f\u044b': 'Folder', # ФормаГруппы
}
# Mutual exclusion validation
if args.FromObject and args.JsonPath:
print("Cannot use both -JsonPath and -FromObject. Choose one mode.", file=sys.stderr)
sys.exit(1)
if not args.FromObject and not args.JsonPath:
print("Either -JsonPath or -FromObject is required.", file=sys.stderr)
sys.exit(1)
# Normalize OutputPath in from-object mode: append /Ext/Form.xml if missing
if args.FromObject:
out_norm = args.OutputPath.rstrip('/\\')
if not re.search(r'[/\\]Ext[/\\]Form\.xml$', out_norm):
if re.search(r'[/\\]Ext$', out_norm):
args.OutputPath = out_norm + '/Form.xml'
else:
args.OutputPath = out_norm + '/Ext/Form.xml'
print(f"[resolved] OutputPath -> {args.OutputPath}")
# --- Detect XML format version --- # --- Detect XML format version ---
out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath) out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath)
format_version = detect_format_version(os.path.dirname(out_path_resolved)) format_version = detect_format_version(os.path.dirname(out_path_resolved))
# --- 1. Load and validate JSON --- # --- 0. From-object mode ---
json_path = args.JsonPath if args.FromObject:
if not os.path.exists(json_path): # Resolve object path and purpose from OutputPath convention:
print(f"File not found: {json_path}", file=sys.stderr) # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
sys.exit(1) out_abs = out_path_resolved
parts = re.split(r'[/\\]', out_abs)
forms_idx = -1
for i in range(len(parts) - 1, -1, -1):
if parts[i] == 'Forms':
forms_idx = i
break
with open(json_path, 'r', encoding='utf-8-sig') as f: resolved_object_path = None
defn = json.load(f) resolved_purpose = None
if forms_idx >= 2:
form_name = parts[forms_idx + 1]
object_name = parts[forms_idx - 1]
type_plural_and_above = os.sep.join(parts[:forms_idx - 1])
if form_name in _FORM_NAME_TO_PURPOSE:
resolved_purpose = _FORM_NAME_TO_PURPOSE[form_name]
candidate = os.path.join(type_plural_and_above, f'{object_name}.xml')
if os.path.exists(candidate):
resolved_object_path = candidate
# Apply: explicit -ObjectPath / -Purpose override resolved
from_obj_path = None
if args.ObjectPath:
from_obj_path = args.ObjectPath if os.path.isabs(args.ObjectPath) else os.path.join(os.getcwd(), args.ObjectPath)
if not from_obj_path.endswith('.xml'):
from_obj_path += '.xml'
elif resolved_object_path:
from_obj_path = resolved_object_path
print(f"[resolved] ObjectPath -> {from_obj_path}")
else:
print("Cannot derive object path from OutputPath. Use -ObjectPath explicitly.", file=sys.stderr)
sys.exit(1)
if not os.path.exists(from_obj_path):
print(f"Object file not found: {from_obj_path}", file=sys.stderr)
sys.exit(1)
purpose = args.Purpose or resolved_purpose or 'Item'
if resolved_purpose and not args.Purpose:
print(f"[resolved] Purpose -> {purpose}")
meta = parse_object_meta(from_obj_path)
print(f"[from-object] Type={meta['Type']}, Name={meta['Name']}, Attrs={len(meta['Attributes'])}, TS={len(meta['TabularSections'])}")
preset_data = load_preset(args.Preset, os.path.dirname(os.path.abspath(__file__)), out_path_resolved)
supported = {'Document': ['Item', 'List', 'Choice'], 'Catalog': ['Item', 'Folder', 'List', 'Choice']}
if meta['Type'] not in supported:
print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog.", file=sys.stderr)
sys.exit(1)
if purpose not in supported[meta['Type']]:
print(f"Purpose '{purpose}' not valid for {meta['Type']}. Valid: {', '.join(supported[meta['Type']])}", file=sys.stderr)
sys.exit(1)
if meta['Type'] == 'Document':
dsl = generate_document_dsl(meta, preset_data, purpose)
else:
dsl = generate_catalog_dsl(meta, preset_data, purpose)
if args.EmitDsl:
dsl_path = args.EmitDsl if os.path.isabs(args.EmitDsl) else os.path.join(os.getcwd(), args.EmitDsl)
os.makedirs(os.path.dirname(dsl_path) or '.', exist_ok=True)
with open(dsl_path, 'w', encoding='utf-8') as f:
json.dump(dsl, f, ensure_ascii=False, indent=2)
print(f"[from-object] DSL saved: {dsl_path}")
defn = json.loads(json.dumps(dsl)) # normalize OrderedDict to regular dict
else:
# --- 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 --- # --- 2. Main compilation ---
_next_id = 0 _next_id = 0