diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index e5aaa5ea..2015a947 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -1,7 +1,7 @@ --- name: form-compile -description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов -argument-hint: +description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму +argument-hint: | -FromObject allowed-tools: - Bash - Read @@ -9,29 +9,30 @@ allowed-tools: - Glob --- -# /form-compile — Генерация Form.xml из JSON DSL +# /form-compile — Генерация Form.xml -Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100–500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID. +Два режима: +1. **JSON DSL** — из JSON-определения формы +2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP -> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно. - -## Использование - -``` -/form-compile -``` +> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно. ## Параметры -| Параметр | Обязательный | Описание | -|------------|:------------:|-----------------------------------| -| JsonPath | да | Путь к JSON-определению формы | -| OutputPath | да | Путь к выходному файлу Form.xml | +| Параметр | Обязательный | Описание | +|------------|:------------:|---------------------------------| +| JsonPath | режим 1 | Путь к JSON-определению формы | +| OutputPath | да | Путь к выходному Form.xml | +| FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта | ## Команда ```powershell -powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "" -OutputPath "" +# Режим JSON DSL +powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "" -OutputPath "" + +# Режим 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 — справка @@ -167,6 +168,12 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile | `footer: true` | Показать подвал | | `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` | | `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` | +| `choiceMode: true` | Режим выбора (для форм выбора) | +| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) | +| `enableDrag: true` | Разрешить перетаскивание | +| `enableStartDrag: true` | Разрешить начало перетаскивания | +| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) | +| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar | ### Страницы (pages + page) @@ -221,6 +228,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile ```json { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true } +{ "name": "Список", "type": "DynamicList", "main": true, "settings": { + "mainTable": "Catalog.Номенклатура", "dynamicDataRead": true +}} { "name": "Итого", "type": "decimal(15,2)" } { "name": "Таблица", "type": "ValueTable", "columns": [ { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, diff --git a/.claude/skills/form-compile/presets/erp-standard.json b/.claude/skills/form-compile/presets/erp-standard.json new file mode 100644 index 00000000..0a9e4f9d --- /dev/null +++ b/.claude/skills/form-compile/presets/erp-standard.json @@ -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": ["ДополнительныеРеквизиты", "Представления"] + } + } +} diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 149ed46f..c434aa33 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,16 +1,822 @@ -# 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 param( - [Parameter(Mandatory)] [string]$JsonPath, [Parameter(Mandatory)] - [string]$OutputPath + [string]$OutputPath, + + [switch]$FromObject, + [string]$ObjectPath, + [string]$Purpose, + [string]$Preset = "erp-standard", + [string]$EmitDsl ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# ═══════════════════════════════════════════════════════════════════════════ +# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation +# ═══════════════════════════════════════════════════════════════════════════ + +function Parse-ObjectMeta([string]$ObjectPath) { + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($ObjectPath) + + $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + + # Detect object type from root child + $metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns) + if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 } + $typeNode = $metaRoot.FirstChild + $objType = $typeNode.LocalName # "Document", "Catalog", etc. + + $propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) + $childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) + + # Name + $objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText + + # Synonym (Russian) + $synonym = $objName + $synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + if ($synNode) { $synonym = $synNode.InnerText } + + # Helper: extract type string from md:Type + $extractType = { + param($typeParent) + if (-not $typeParent) { return "string" } + $types = @() + foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) { + $types += $t.InnerText + } + if ($types.Count -eq 0) { return "string" } + return ($types -join " | ") + } + + # Helper: check if type is a reference + $isRefType = { + param([string]$t) + return ($t -match 'Ref\.' -or $t -match 'ссылка\.') + } + + # Helper: extract attribute list from ChildObjects + $extractAttrs = { + param($parentNode) + $result = @() + if (-not $parentNode) { return $result } + foreach ($attrNode in $parentNode.SelectNodes("md:Attribute", $ns)) { + $ap = $attrNode.SelectSingleNode("md:Properties", $ns) + $aName = $ap.SelectSingleNode("md:Name", $ns).InnerText + $aSynNode = $ap.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $aSyn = if ($aSynNode) { $aSynNode.InnerText } else { $aName } + $aTypeNode = $ap.SelectSingleNode("md:Type", $ns) + $aType = & $extractType $aTypeNode + $result += @{ + Name = $aName + Synonym = $aSyn + Type = $aType + IsRef = (& $isRefType $aType) + } + } + return $result + } + + # Attributes + $attributes = @(& $extractAttrs $childObjs) + + # Tabular sections + $tabularSections = @() + if ($childObjs) { + foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) { + $tsp = $tsNode.SelectSingleNode("md:Properties", $ns) + $tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText + $tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName } + $tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns) + $tsCols = @(& $extractAttrs $tsCo) + $tabularSections += @{ + Name = $tsName + Synonym = $tsSyn + Columns = $tsCols + } + } + } + + $meta = @{ + Type = $objType + Name = $objName + Synonym = $synonym + Attributes = $attributes + TabularSections = $tabularSections + } + + # Type-specific properties + switch ($objType) { + "Document" { + $ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns) + $meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" } + } + "Catalog" { + $clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns) + $meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 } + $dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns) + $meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 } + $hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns) + $meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true") + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + # Owners + $owners = @() + foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { + $owners += $ow.InnerText + } + $meta.Owners = $owners + } + } + + return $meta +} + +function Load-Preset([string]$PresetName, [string]$ScriptDir) { + # Hardcoded defaults (ERP-oriented) + $defaults = @{ + "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 } + 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 = "Входит в группу"; position = "afterCodeDescription" } + owner = @{ readOnly = $true; position = "first" } + tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); 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 = "Входит в группу" } + 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" } + } + } + + # Deep merge helper + $deepMerge = { + param($base, $overlay) + if (-not $overlay) { return $base } + if (-not $base) { return $overlay } + $result = @{} + foreach ($k in $base.Keys) { $result[$k] = $base[$k] } + foreach ($k in $overlay.Keys) { + if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { + $result[$k] = & $deepMerge $result[$k] $overlay[$k] + } else { + $result[$k] = $overlay[$k] + } + } + return $result + } + + # Try built-in preset + $presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets" + $builtInPath = Join-Path $presetDir "$PresetName.json" + if (Test-Path $builtInPath) { + $presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json + # Convert PSCustomObject to hashtable recursively + $toHash = { + param($obj) + if ($obj -is [System.Management.Automation.PSCustomObject]) { + $h = @{} + foreach ($p in $obj.PSObject.Properties) { + $h[$p.Name] = & $toHash $p.Value + } + return $h + } + if ($obj -is [System.Object[]]) { + return @($obj | ForEach-Object { & $toHash $_ }) + } + return $obj + } + $presetHash = & $toHash $presetJson + foreach ($k in @($presetHash.Keys)) { + $defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k] + } + } + + # Try project-level preset (scan up from output path) + $scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved) + while ($scanDir) { + $projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json" + if (Test-Path $projPreset) { + $projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json + $projHash = & $toHash $projJson + foreach ($k in @($projHash.Keys)) { + $defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k] + } + break + } + $parentDir = Split-Path $scanDir -Parent + if ($parentDir -eq $scanDir) { break } + $scanDir = $parentDir + } + + # Resolve basedOn references + foreach ($k in @($defaults.Keys)) { + $sect = $defaults[$k] + if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) { + $baseName = $sect["basedOn"] + if ($defaults.ContainsKey($baseName)) { + $merged = & $deepMerge $defaults[$baseName] $sect + $merged.Remove("basedOn") + $defaults[$k] = $merged + } + } + } + + return $defaults +} + +# --- Helper: build a field element DSL entry --- +function New-FieldElement { + param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps) + + $isRef = ($attrType -match 'Ref\.') + $isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean') + + # Determine element type + $elType = "input" + if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") { + $elType = "check" + } + + $el = [ordered]@{ $elType = $attrName; path = $dataPath } + + # Apply ref defaults + if ($isRef -and $fieldDefaults -and $fieldDefaults.ref) { + if ($fieldDefaults.ref.choiceButton -eq $true) { $el["choiceButton"] = $true } + } + + # Extra props + if ($extraProps) { + foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] } + } + + return $el +} + +# --- Catalog DSL generators --- +function Generate-CatalogDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + + $purposeKey = "catalog.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } + $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } + + switch ($purpose) { + "Folder" { return Generate-CatalogFolderDSL $meta $p } + "List" { return Generate-CatalogListDSL $meta $p } + "Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData } + "Item" { return Generate-CatalogItemDSL $meta $p $fd } + } +} + +function Generate-CatalogFolderDSL($meta, [hashtable]$p) { + $elements = @() + # Code (if CodeLength > 0) + if ($meta.CodeLength -gt 0) { + $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + # Description + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + # Parent + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } + $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } + if ($parentTitle) { $parentEl["title"] = $parentTitle } + $elements += $parentEl + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + $formProps = [ordered]@{ useForFoldersAndItems = "Folders" } + foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $elements + attributes = @( + [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } + ) + } +} + +function Generate-CatalogListDSL($meta, [hashtable]$p) { + # Columns + $columns = @() + # Description always first + $columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" } + # Code if present + if ($meta.CodeLength -gt 0) { + $columns += [ordered]@{ labelField = "Код"; path = "Список.Code" } + } + # Custom attributes + foreach ($attr in $meta.Attributes) { + $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } + } + # Hidden ref + if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { + $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; visible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + # Hierarchical properties + if ($meta.Hierarchical) { + $tableEl["initialTreeView"] = "ExpandTopLevel" + $tableEl["enableStartDrag"] = $true + $tableEl["enableDrag"] = $true + } + + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = @($tableEl) + attributes = @( + [ordered]@{ + name = "Список"; type = "DynamicList"; main = $true + settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true } + } + ) + } +} + +function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { + # Start from list + $listKey = "catalog.list" + $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } + $dsl = Generate-CatalogListDSL $meta $lp + + # Add choice-specific properties + $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" + if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } + + # Set ChoiceMode on table + $dsl.elements[0]["choiceMode"] = $true + + return $dsl +} + +function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) { + $headerChildren = @() + + # Owner (if subordinate) + if ($meta.Owners -and $meta.Owners.Count -gt 0) { + $ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true } + $headerChildren += $ownerEl + } + + # Code + Description + $cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" } + $cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" } + $hasCode = ($meta.CodeLength -gt 0) + + if ($cdLayout -eq "horizontal" -and $hasCode) { + $cdChildren = @() + $descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" } + $codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" } + if ($cdOrder -eq "descriptionFirst") { + $cdChildren = @($descEl, $codeEl) + } else { + $cdChildren = @($codeEl, $descEl) + } + $headerChildren += [ordered]@{ + group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false + representation = "none"; children = $cdChildren + } + } else { + # Vertical or no code + $headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + if ($hasCode) { + $headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + } + + # Parent (for hierarchical catalogs) + $parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" } + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null } + if ($meta.Hierarchical) { + $parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" } + if ($parentTitle) { $parentEl["title"] = $parentTitle } + if ($parentPos -eq "beforeCodeDescription") { + # Insert before Code/Description (after Owner if present) + $insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 } + $newChildren = @() + for ($i = 0; $i -lt $headerChildren.Count; $i++) { + if ($i -eq $insertIdx) { $newChildren += $parentEl } + $newChildren += $headerChildren[$i] + } + $headerChildren = $newChildren + } else { + # afterCodeDescription (default) + $headerChildren += $parentEl + } + } + + # Custom attributes → header + $footerFieldNames = @() + if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) } + + foreach ($attr in $meta.Attributes) { + if ($footerFieldNames -contains $attr.Name) { continue } + $headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Build root elements + $rootElements = @() + + # ГруппаШапка + $rootElements += [ordered]@{ + group = "vertical"; name = "ГруппаШапка"; showTitle = $false + representation = "none"; children = $headerChildren + } + + # Tabular sections + $tsExclude = @("ДополнительныеРеквизиты", "Представления") + if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } + $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } + $tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" } + + $visibleTS = @() + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $visibleTS += $ts + } + + foreach ($ts in $visibleTS) { + $tsCols = @() + if ($tsLineNumber) { + $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } + } + foreach ($col in $ts.Columns) { + $colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{} + $tsCols += $colEl + } + $tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + $rootElements += $tableEl + } + + # Footer fields + foreach ($fn in $footerFieldNames) { + $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } + if ($fAttr) { + $rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + + # BSP group + $bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } + if ($bspGroup) { + $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + + # Properties + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + # UseForFoldersAndItems + if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") { + $formProps["useForFoldersAndItems"] = "Items" + } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $rootElements + attributes = @( + [ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true } + ) + } +} + +# --- Document DSL generators --- +function Generate-DocumentDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + + $purposeKey = "document.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} } + $fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} } + + switch ($purpose) { + "List" { return Generate-DocumentListDSL $meta $p } + "Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData } + "Item" { return Generate-DocumentItemDSL $meta $p $fd } + } +} + +function Generate-DocumentListDSL($meta, [hashtable]$p) { + $columns = @() + # All custom attributes as labelField + foreach ($attr in $meta.Attributes) { + $columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" } + } + # Hidden ref + if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) { + $columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; visible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $formProps = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = @($tableEl) + attributes = @( + [ordered]@{ + name = "Список"; type = "DynamicList"; main = $true + settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true } + } + ) + } +} + +function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) { + $listKey = "document.list" + $lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} } + $dsl = Generate-DocumentListDSL $meta $lp + + $dsl.properties["windowOpeningMode"] = "LockOwnerWindow" + if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } } + + return $dsl +} + +function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) { + $headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" } + $headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" } + $headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" } + $dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" } + + $footerFields = @() + if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) } + $footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" } + + $addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" } + $addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" } + $addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true } + $addLeft = @(); $addRight = @() + if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) } + if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) } + + $headerRight = @() + if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) } + + $tsExclude = @("ДополнительныеРеквизиты") + if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) } + $tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true } + + # Classify attributes + $claimed = @{} + foreach ($fn in $footerFields) { $claimed[$fn] = "footer" } + foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" } + foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" } + foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" } + + $unclaimed = @() + foreach ($attr in $meta.Attributes) { + if (-not $claimed.ContainsKey($attr.Name)) { $unclaimed += $attr } + } + + # Distribute unclaimed + $leftAttrs = @(); $rightExtraAttrs = @() + switch ($headerDistribute) { + "left" { $leftAttrs = $unclaimed } + "right" { $rightExtraAttrs = $unclaimed } + default { # "even" + $half = [Math]::Ceiling($unclaimed.Count / 2) + for ($i = 0; $i -lt $unclaimed.Count; $i++) { + if ($i -lt $half) { $leftAttrs += $unclaimed[$i] } + else { $rightExtraAttrs += $unclaimed[$i] } + } + } + } + + # Build ГруппаНомерДата + $numDateChildren = @( + [ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 } + [ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle } + ) + $numDateGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren + } + + # Build left column + $leftChildren = @($numDateGroup) + foreach ($attr in $leftAttrs) { + $leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Build right column + $rightChildren = @() + foreach ($rn in $headerRight) { + $rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn } + if ($rAttr) { + $rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + foreach ($attr in $rightExtraAttrs) { + $rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{}) + } + + # Header group + $headerGroup = $null + if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) { + $headerGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren } + [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren } + ) + } + } else { + # 1col or no right items + $allHeaderFields = $leftChildren + $rightChildren + $headerGroup = [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields } + ) + } + } + + # Footer elements + $footerElements = @() + foreach ($fn in $footerFields) { + $fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn } + if ($fAttr) { + $footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + + # Visible tabular sections + $visibleTS = @() + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $visibleTS += $ts + } + + # Additional page content + $additionalPage = $null + if ($addPos -eq "page") { + $addLeftEls = @(); $addRightEls = @() + foreach ($aln in $addLeft) { + $alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln } + if ($alAttr) { + $addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + foreach ($arn in $addRight) { + $arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn } + if ($arAttr) { + $addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{}) + } + } + $addPageChildren = @() + if ($addLayout -eq "2col") { + $addPageChildren += [ordered]@{ + group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls } + [ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls } + ) + } + } else { + $addPageChildren += @($addLeftEls + $addRightEls) + } + if ($addBspGroup) { + $addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + $additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren } + } + + # Build TS page elements + $tsPages = @() + foreach ($ts in $visibleTS) { + $tsCols = @() + if ($tsLineNumber) { + $tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" } + } + foreach ($col in $ts.Columns) { + $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}) + } + $tsPages += [ordered]@{ + page = "Группа$($ts.Name)"; title = $ts.Synonym + children = @( + [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + ) + } + } + + # Assemble root elements + $rootElements = @() + + if ($visibleTS.Count -eq 0) { + # Simple form — no Pages + $rootElements += $headerGroup + if ($footerElements.Count -gt 0) { $rootElements += $footerElements } + if ($addBspGroup -and $addPos -ne "none") { + $rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" } + } + } else { + # Pages form + if ($headerPos -eq "abovePages") { + $rootElements += $headerGroup + $pagesChildren = @() + $pagesChildren += $tsPages + if ($additionalPage) { $pagesChildren += $additionalPage } + $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } + } else { + # insidePage (default) + $osnovnoeChildren = @($headerGroup) + if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) { + $osnovnoeChildren += $footerElements + } + $pagesChildren = @() + $pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren } + $pagesChildren += $tsPages + if ($additionalPage) { $pagesChildren += $additionalPage } + $rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren } + } + + # Footer below pages + if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) { + $rootElements += $footerElements + } + } + + # Properties + $formProps = [ordered]@{ autoTitle = $false } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $formProps + elements = $rootElements + attributes = @( + [ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true } + ) + } +} + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + # --- Detect XML format version --- function Detect-FormatVersion([string]$dir) { @@ -31,15 +837,140 @@ function Detect-FormatVersion([string]$dir) { $script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } $script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved)) -# --- 1. Load and validate JSON --- +# --- 0. Path normalization and mode dispatch --- -if (-not (Test-Path $JsonPath)) { - Write-Error "File not found: $JsonPath" +# Form name → purpose mapping +$script:formNameToPurpose = @{ + "ФормаДокумента" = "Item" + "ФормаЭлемента" = "Item" + "ФормаСписка" = "List" + "ФормаВыбора" = "Choice" + "ФормаГруппы" = "Folder" +} + +if ($FromObject -and $JsonPath) { + Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode." + exit 1 +} +if (-not $FromObject -and -not $JsonPath) { + Write-Error "Either -JsonPath or -FromObject is required." exit 1 } -$json = Get-Content -Raw -Encoding UTF8 $JsonPath -$def = $json | ConvertFrom-Json +if ($FromObject) { + # Normalize OutputPath: append /Ext/Form.xml if missing + $outNorm = $OutputPath -replace '[\\/]$', '' + if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') { + if ($outNorm -match '[/\\]Ext$') { + $OutputPath = "$outNorm/Form.xml" + } else { + $OutputPath = "$outNorm/Ext/Form.xml" + } + Write-Host "[resolved] OutputPath -> $OutputPath" + } + + # Resolve object path and purpose from OutputPath convention: + # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + $outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } + $pathParts = $outAbs -split '[/\\]' + # Find "Forms" segment + $formsIdx = -1 + for ($i = $pathParts.Count - 1; $i -ge 0; $i--) { + if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break } + } + + $resolvedObjectPath = $null + $resolvedPurpose = $null + + if ($formsIdx -ge 2) { + $formName = $pathParts[$formsIdx + 1] + $objectName = $pathParts[$formsIdx - 1] + $typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar + + # Derive purpose from form name + if ($script:formNameToPurpose.ContainsKey($formName)) { + $resolvedPurpose = $script:formNameToPurpose[$formName] + } + + # Derive object XML path + $candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml" + if (Test-Path $candidateObjPath) { + $resolvedObjectPath = $candidateObjPath + } + } + + # Apply: explicit -ObjectPath / -Purpose override resolved values + $fromObjPath = $null + if ($ObjectPath) { + $fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath } + # Append .xml if missing + if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" } + } elseif ($resolvedObjectPath) { + $fromObjPath = $resolvedObjectPath + Write-Host "[resolved] ObjectPath -> $fromObjPath" + } else { + Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly." + exit 1 + } + + if (-not (Test-Path $fromObjPath)) { + Write-Error "Object file not found: $fromObjPath" + exit 1 + } + + $effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" } + if ($resolvedPurpose -and -not $Purpose) { + Write-Host "[resolved] Purpose -> $effectivePurpose" + } + + $meta = Parse-ObjectMeta $fromObjPath + Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)" + + $presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot + + $supportedPurposes = switch ($meta.Type) { + "Document" { @("Item","List","Choice") } + "Catalog" { @("Item","Folder","List","Choice") } + default { @() } + } + if ($supportedPurposes.Count -eq 0) { + Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog." + exit 1 + } + if ($supportedPurposes -notcontains $effectivePurpose) { + Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')" + exit 1 + } + + # Generate DSL + $dsl = switch ($meta.Type) { + "Document" { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "Catalog" { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + } + + # Emit DSL if requested + if ($EmitDsl) { + $dslJson = $dsl | ConvertTo-Json -Depth 20 + $dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl } + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($dslPath, $dslJson, $enc) + Write-Host "[from-object] DSL saved: $dslPath" + } + + # Feed DSL into existing compiler + $dslJson = $dsl | ConvertTo-Json -Depth 20 + $def = $dslJson | ConvertFrom-Json +} else { + # --- 1. Load and validate JSON (original mode) --- + + if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 + } + + $json = Get-Content -Raw -Encoding UTF8 $JsonPath + $def = $json | ConvertFrom-Json +} # --- 2. ID allocator --- @@ -411,6 +1342,8 @@ function Emit-Element { # table-specific "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 "commandBarLocation"=1;"searchStringLocation"=1 + "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 + "rowPictureDataPath"=1;"tableAutofill"=1 # pages-specific "pagesRepresentation"=1 # button-specific @@ -677,10 +1610,24 @@ function Emit-Table { if ($el.searchStringLocation) { X "$inner$($el.searchStringLocation)" } + if ($el.choiceMode -eq $true) { X "$innertrue" } + if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } + if ($el.enableStartDrag -eq $true) { X "$innertrue" } + if ($el.enableDrag -eq $true) { X "$innertrue" } + if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } # Companions Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + # AutoCommandBar — with optional Autofill control + if ($null -ne $el.tableAutofill) { + $acbId = New-Id + X "$inner" + $afVal = if ($el.tableAutofill) { "true" } else { "false" } + X "$inner`t$afVal" + X "$inner" + } else { + Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + } Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner @@ -1000,6 +1947,18 @@ function Emit-Attributes { X "$inner" } + # Settings (for DynamicList) + if ($attr.settings) { + X "$inner" + $si = "$inner`t" + if ($attr.settings.mainTable) { X "$si$($attr.settings.mainTable)" } + $mq = if ($attr.settings.manualQuery -eq $true) { "true" } else { "false" } + X "$si$mq" + $ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" } + X "$si$ddr" + X "$inner" + } + X "$indent`t" } X "$indent" diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 16352658..96ad51db 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,1159 +1,2054 @@ -#!/usr/bin/env python3 -# form-compile v1.4 — 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|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' - r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' - r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' - r'InformationRegisterRecordSet|InformationRegisterRecordManager|' - r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' - r'ConstantsSet|DataProcessorObject|ReportObject)\.' -) - -KNOWN_INVALID_TYPES = { - 'FormDataStructure': 'Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)', - 'FormDataCollection': 'Runtime type. Use ValueTable', - 'FormDataTree': 'Runtime type. Use ValueTree', - 'FormDataTreeItem': 'Runtime type, not valid in XML', - 'FormDataCollectionItem': 'Runtime type, not valid in XML', - 'FormGroup': 'UI element type, not a data type', - 'FormField': 'UI element type, not a data type', - 'FormButton': 'UI element type, not a data type', - 'FormDecoration': 'UI element type, not a data type', - 'FormTable': 'UI element type, not a data type', -} - - -_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 with validation - if type_str in KNOWN_INVALID_TYPES: - raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") - if '.' in type_str: - lines.append(f'{indent}cfg:{type_str}') - else: - print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) - 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}
true
') - - 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 detect_format_version(d): - while d: - cfg_path = os.path.join(d, "Configuration.xml") - if os.path.isfile(cfg_path): - with open(cfg_path, "r", encoding="utf-8-sig") as f: - head = f.read(2000) - m = re.search(r']+version="(\d+\.\d+)"', head) - if m: - return m.group(1) - parent = os.path.dirname(d) - if parent == d: - break - d = parent - return "2.17" - - -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() - - # --- Detect XML format version --- - 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)) - - # --- 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(f'
') - - # 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() +#!/usr/bin/env python3 +# form-compile v1.5 — Compile 1C managed form from JSON or object metadata +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +import argparse +import copy +import json +import os +import re +import sys +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): + 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", + "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", + "rowPictureDataPath", "tableAutofill", +} + +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|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|' + r'ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|' + r'ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|' + r'InformationRegisterRecordSet|InformationRegisterRecordManager|' + r'AccumulationRegisterRecordSet|AccountingRegisterRecordSet|' + r'ConstantsSet|DataProcessorObject|ReportObject)\.' +) + +KNOWN_INVALID_TYPES = { + 'FormDataStructure': 'Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)', + 'FormDataCollection': 'Runtime type. Use ValueTable', + 'FormDataTree': 'Runtime type. Use ValueTree', + 'FormDataTreeItem': 'Runtime type, not valid in XML', + 'FormDataCollectionItem': 'Runtime type, not valid in XML', + 'FormGroup': 'UI element type, not a data type', + 'FormField': 'UI element type, not a data type', + 'FormButton': 'UI element type, not a data type', + 'FormDecoration': 'UI element type, not a data type', + 'FormTable': 'UI element type, not a data type', +} + + +_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 with validation + if type_str in KNOWN_INVALID_TYPES: + raise ValueError(f"Invalid form attribute type '{type_str}': {KNOWN_INVALID_TYPES[type_str]}") + if '.' in type_str: + lines.append(f'{indent}cfg:{type_str}') + else: + print(f"WARNING: Unrecognized bare type '{type_str}' — will be emitted without namespace prefix", file=sys.stderr) + 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}
true
') + + if el.get('commandBarLocation'): + lines.append(f'{inner}{el["commandBarLocation"]}') + if el.get('searchStringLocation'): + lines.append(f'{inner}{el["searchStringLocation"]}') + + if el.get('choiceMode') is True: + lines.append(f'{inner}true') + if el.get('initialTreeView'): + lines.append(f'{inner}{el["initialTreeView"]}') + if el.get('enableStartDrag') is True: + lines.append(f'{inner}true') + if el.get('enableDrag') is True: + lines.append(f'{inner}true') + if el.get('rowPictureDataPath'): + lines.append(f'{inner}{el["rowPictureDataPath"]}') + + # Companions + emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', 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}') + lines.append(f'{inner}\t{af_val}') + lines.append(f'{inner}') + 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, '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}') + + # Settings (for DynamicList) + if attr.get('settings'): + s = attr['settings'] + lines.append(f'{inner}') + si = f'{inner}\t' + if s.get('mainTable'): + lines.append(f'{si}{s["mainTable"]}') + mq = 'true' if s.get('manualQuery') else 'false' + lines.append(f'{si}{mq}') + ddr = 'true' if s.get('dynamicDataRead') else 'false' + lines.append(f'{si}{ddr}') + 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 detect_format_version(d): + while d: + cfg_path = os.path.join(d, "Configuration.xml") + if os.path.isfile(cfg_path): + with open(cfg_path, "r", encoding="utf-8-sig") as f: + head = f.read(2000) + m = re.search(r']+version="(\d+\.\d+)"', head) + if m: + return m.group(1) + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return "2.17" + + +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 or object metadata', allow_abbrev=False) + parser.add_argument('-JsonPath', type=str, default=None) + 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() + + # 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 --- + 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)) + + # --- 0. From-object mode --- + if args.FromObject: + # Resolve object path and purpose from OutputPath convention: + # .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml + 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 + + resolved_object_path = None + 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 --- + _next_id = 0 + lines = [] + + lines.append('') + lines.append(f'
') + + # 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()