diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 2f20ae0b..cc4157e6 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,6203 +1,6222 @@ -# form-compile v1.137 — Compile 1C managed form from JSON or object metadata -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -param( - [string]$JsonPath, - - [Parameter(Mandatory)] - [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 field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag) - $extractFields = { - param($parentNode, [string]$tagName) - $result = @() - if (-not $parentNode) { return $result } - foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) { - $fp = $fieldNode.SelectSingleNode("md:Properties", $ns) - $fName = $fp.SelectSingleNode("md:Name", $ns).InnerText - $fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) - $fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName } - $fTypeNode = $fp.SelectSingleNode("md:Type", $ns) - $fType = & $extractType $fTypeNode - $result += @{ - Name = $fName - Synonym = $fSyn - Type = $fType - IsRef = (& $isRefType $fType) - } - } - return $result - } - - # Attributes - $attributes = @(& $extractFields $childObjs "Attribute") - - # 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 = @(& $extractFields $tsCo "Attribute") - $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 - } - "InformationRegister" { - $meta.Dimensions = @(& $extractFields $childObjs "Dimension") - $meta.Resources = @(& $extractFields $childObjs "Resource") - $prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) - $meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" } - $wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns) - $meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" } - } - "AccumulationRegister" { - $meta.Dimensions = @(& $extractFields $childObjs "Dimension") - $meta.Resources = @(& $extractFields $childObjs "Resource") - $rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns) - $meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" } - } - "ChartOfCharacteristicTypes" { - $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 = @() - foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { - $owners += $ow.InnerText - } - $meta.Owners = $owners - $meta.HasValueType = $true - } - "ExchangePlan" { - $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 } - $meta.Hierarchical = $false - $meta.HierarchyType = $null - $meta.Owners = @() - } - "ChartOfAccounts" { - $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 } - $meta.Hierarchical = $true - $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) - $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } - $meta.Owners = @() - $maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) - $meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 } - $meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag") - $meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag") - } - } - - 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" } - } - # ─── Register defaults ─── - "informationRegister.record" = @{ - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "informationRegister.list" = @{ - columns = "all"; columnType = "labelField" - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - "accumulationRegister.list" = @{ - columns = "all"; columnType = "labelField" - tableCommandBar = "none"; commandBar = "auto" - properties = @{} - } - # ─── Catalog-like type defaults ─── - "chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" } - "chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" } - "chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" } - "chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" } - "exchangePlan.item" = @{ basedOn = "catalog.item" } - "exchangePlan.list" = @{ basedOn = "catalog.list" } - "exchangePlan.choice" = @{ basedOn = "catalog.choice" } - # ─── ChartOfAccounts defaults ─── - "chartOfAccounts.item" = @{ - parent = @{ title = "Подчинен счету" } - fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } - properties = @{} - } - "chartOfAccounts.folder" = @{ - parent = @{ title = "Подчинен счету" } - properties = @{ windowOpeningMode = "LockOwnerWindow" } - } - "chartOfAccounts.list" = @{ basedOn = "catalog.list" } - "chartOfAccounts.choice" = @{ basedOn = "catalog.choice" } - } - - # 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 --- -# Non-displayable types — cannot be bound to form elements -$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения') - -function Test-DisplayableType([string]$typeStr) { - foreach ($nd in $script:nonDisplayableTypes) { - if ($typeStr -match [regex]::Escape($nd)) { return $false } - } - return $true -} - -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 } - - # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. - # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы - # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) - - # 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) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $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"; userVisible = $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 } - if (-not (Test-DisplayableType $attr.Type)) { 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 = @() - # Standard columns: Number + Date - $columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" } - $columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" } - # All custom attributes as labelField - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $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"; userVisible = $false } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - 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) -and (Test-DisplayableType $attr.Type)) { $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 -and (Test-DisplayableType $fAttr.Type)) { - $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 } - ) - } -} - -# ─── InformationRegister ────────────────────────────────────────────────── - -function Generate-InformationRegisterDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "informationRegister.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } - switch ($purpose) { - "Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd } - "List" { return Generate-InformationRegisterListDSL $meta $p } - } -} - -function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) { - $elements = @() - $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" - - # Period first (if periodic) - if ($isPeriodic) { - $elements += [ordered]@{ input = "Период"; path = "Запись.Period" } - } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd) - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd) - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) - } - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = $elements - attributes = @( - @{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-InformationRegisterListDSL($meta, [hashtable]$p) { - $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" - $isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate" - - $columns = @() - # Period - if ($isPeriodic) { - $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } - } - # Recorder/LineNumber for subordinate registers - if ($isRecorderSubordinate) { - $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } - $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } - } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elKey = "labelField" - if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elKey = "labelField" - if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = @($tableEl) - attributes = @( - @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } } - ) - } -} - -# ─── AccumulationRegister ───────────────────────────────────────────────── - -function Generate-AccumulationRegisterDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "accumulationRegister.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - switch ($purpose) { - "List" { return Generate-AccumulationRegisterListDSL $meta $p } - } -} - -function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) { - $columns = @() - # AccumulationRegisters always have Period, Recorder, LineNumber - $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } - $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } - $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } - # Dimensions - foreach ($dim in $meta.Dimensions) { - if (-not (Test-DisplayableType $dim.Type)) { continue } - $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } - } - # Resources - foreach ($res in $meta.Resources) { - if (-not (Test-DisplayableType $res.Type)) { continue } - $elKey = "labelField" - if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } - } - # Attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elKey = "labelField" - if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } - $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } - } - - $tableEl = [ordered]@{ - table = "Список"; path = "Список" - rowPictureDataPath = "Список.DefaultPicture" - commandBarLocation = "None" - tableAutofill = $false - columns = $columns - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = @($tableEl) - attributes = @( - @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } } - ) - } -} - -# ─── ChartOfCharacteristicTypes (delegates to Catalog) ──────────────────── - -function Generate-ChartOfCharacteristicTypesDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - # Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc. - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose - - # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types - $catObjType = "CatalogObject.$($meta.Name)" - $ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)" - $catListType = "Catalog.$($meta.Name)" - $ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)" - - foreach ($a in $dsl.attributes) { - if ($a.type -eq $catObjType) { $a.type = $ccoctObjType } - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { - $a.settings.mainTable = $ccoctListType - } - } - - # For Item forms: inject ValueType field after Code/Description - if ($purpose -eq "Item" -and $dsl.elements) { - $vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" } - $newElements = @() - $inserted = $false - foreach ($el in $dsl.elements) { - $newElements += $el - if (-not $inserted) { - $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } - if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { - $newElements += $vtEl - $inserted = $true - } - } - } - if (-not $inserted) { $newElements += $vtEl } - $dsl.elements = $newElements - } - - return $dsl -} - -# ─── ExchangePlan (delegates to Catalog) ────────────────────────────────── - -function Generate-ExchangePlanDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - # ExchangePlans are not hierarchical and have no Folder form - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose - - # Post-patch: replace Catalog types with ExchangePlan types - $catObjType = "CatalogObject.$($meta.Name)" - $epObjType = "ExchangePlanObject.$($meta.Name)" - $catListType = "Catalog.$($meta.Name)" - $epListType = "ExchangePlan.$($meta.Name)" - - foreach ($a in $dsl.attributes) { - if ($a.type -eq $catObjType) { $a.type = $epObjType } - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { - $a.settings.mainTable = $epListType - } - } - - # For Item forms: inject SentNo, ReceivedNo after Code/Description - if ($purpose -eq "Item" -and $dsl.elements) { - $sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true } - $recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true } - $newElements = @() - $inserted = $false - foreach ($el in $dsl.elements) { - $newElements += $el - if (-not $inserted) { - $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } - if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { - $newElements += $sentEl - $newElements += $recvEl - $inserted = $true - } - } - } - if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl } - $dsl.elements = $newElements - } - - return $dsl -} - -# ─── ChartOfAccounts ────────────────────────────────────────────────────── - -function Generate-ChartOfAccountsDSL { - param($meta, [hashtable]$presetData, [string]$purpose) - $pKey = "chartOfAccounts.$($purpose.ToLower())" - $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } - $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } - switch ($purpose) { - "Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData } - "Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p } - "List" { return Generate-ChartOfAccountsListDSL $meta $presetData } - "Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData } - } -} - -function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) { - $elements = @() - - # Header: Code + Parent - $headerLeftChildren = @() - if ($meta.CodeLength -gt 0) { - $headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - $headerRightChildren = @() - if ($meta.Hierarchical) { - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } - $headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } - } - - if ($headerRightChildren.Count -gt 0) { - $elements += [ordered]@{ - group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" - children = @( - [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren } - [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren } - ) - } - } elseif ($headerLeftChildren.Count -gt 0) { - $elements += $headerLeftChildren - } - - # Description - if ($meta.DescriptionLength -gt 0) { - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - } - - # OffBalance - $elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" } - - # AccountingFlags as checkboxes - if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) { - $flagChildren = @() - foreach ($flag in $meta.AccountingFlags) { - $flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" } - } - $elements += [ordered]@{ - group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета" - children = $flagChildren - } - } - - # ExtDimensionTypes table - if ($meta.MaxExtDimensionCount -gt 0) { - # Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С), - # иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта. - $edTable = "ВидыСубконто" - $edCols = @() - $edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } - $edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } - if ($meta.ExtDimensionAccountingFlags) { - foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { - $edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } - } - } - $elements += [ordered]@{ - table = $edTable - path = "Объект.ExtDimensionTypes" - columns = $edCols - } - } - - # Custom attributes - foreach ($attr in $meta.Attributes) { - if (-not (Test-DisplayableType $attr.Type)) { continue } - $elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) - } - - # Tabular sections - $tsExclude = @("ДополнительныеРеквизиты","Представления") - foreach ($ts in $meta.TabularSections) { - if ($tsExclude -contains $ts.Name) { continue } - $tsCols = @() - foreach ($col in $ts.Columns) { - if (-not (Test-DisplayableType $col.Type)) { continue } - $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd) - } - $elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } - } - - $props = [ordered]@{} - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - properties = $props - elements = $elements - attributes = @( - @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) { - $elements = @() - if ($meta.CodeLength -gt 0) { - $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } - } - if ($meta.DescriptionLength -gt 0) { - $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } - } - if ($meta.Hierarchical) { - $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } - $elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } - } - - $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } - if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } - - return [ordered]@{ - title = $meta.Synonym - useForFoldersAndItems = "Folders" - properties = $props - elements = $elements - attributes = @( - @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } - ) - } -} - -function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) { - # Delegate to Catalog List and patch types - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List" - foreach ($a in $dsl.attributes) { - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { - $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" - } - } - return $dsl -} - -function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) { - $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice" - foreach ($a in $dsl.attributes) { - if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { - $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" - } - } - return $dsl -} - -# ═══════════════════════════════════════════════════════════════════════════ -# END OF FROM-OBJECT MODE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════ - -# --- Detect XML format version --- - -function Detect-FormatVersion([string]$dir) { - $d = $dir - while ($d) { - $cfgPath = Join-Path $d "Configuration.xml" - if (Test-Path $cfgPath) { - $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) - if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } - } - $parent = Split-Path $d -Parent - if ($parent -eq $d) { break } - $d = $parent - } - return "2.17" -} - -$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)) - -# --- 0. Path normalization and mode dispatch --- - -# Form name → purpose mapping -$script:formNameToPurpose = @{ - "ФормаДокумента" = "Item" - "ФормаЭлемента" = "Item" - "ФормаСписка" = "List" - "ФормаВыбора" = "Choice" - "ФормаГруппы" = "Folder" - "ФормаЗаписи" = "Record" - "ФормаСчета" = "Item" - "ФормаУзла" = "Item" -} - -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 -} - -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") } - "InformationRegister" { @("Record","List") } - "AccumulationRegister" { @("List") } - "ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") } - "ExchangePlan" { @("Item","List","Choice") } - "ChartOfAccounts" { @("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, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts." - 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 } - "InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } - "ChartOfAccounts" { Generate-ChartOfAccountsDSL -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 -} - -# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile) -$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path } -function Resolve-QueryValue { - param([string]$val, [string]$baseDir) - if (-not $val.StartsWith("@")) { return $val } - $filePath = $val.Substring(1) - if ([System.IO.Path]::IsPathRooted($filePath)) { - $candidates = @($filePath) - } else { - $candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath)) - } - foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" - exit 1 -} - -# --- 2. ID allocator --- - -$script:nextId = 1 -function New-Id { - $id = $script:nextId - $script:nextId++ - return $id -} - -# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё -# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. -function Assert-UniqueName { - param([string]$name, [hashtable]$seen, [string]$kind) - if ($seen.ContainsKey($name)) { - Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')" - exit 1 - } - $seen[$name] = $true -} - -# --- 3. XML helper --- - -$script:xml = New-Object System.Text.StringBuilder 8192 - -function X { - param([string]$text) - $script:xml.AppendLine($text) | Out-Null -} - -function Esc-Xml { - # Экранирование ТЕКСТА элемента (, ): только & < > . - # Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально); - # " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов. - param([string]$s) - return $s.Replace('&','&').Replace('<','<').Replace('>','>') -} - -# --- 4. Multilang helper --- - -# Эмитит для значения: строка → один ru-элемент; объект {lang:text} → по элементу на язык. -function Emit-MLItems { - param($val, [string]$indent) - if ($val -is [System.Collections.IDictionary]) { - foreach ($k in $val.Keys) { - X "$indent"; X "$indent`t$k"; X "$indent`t$(Esc-Xml "$($val[$k])")"; X "$indent" - } - } elseif ($val -is [System.Management.Automation.PSCustomObject]) { - foreach ($p in $val.PSObject.Properties) { - X "$indent"; X "$indent`t$($p.Name)"; X "$indent`t$(Esc-Xml "$($p.Value)")"; X "$indent" - } - } else { - X "$indent"; X "$indent`tru"; X "$indent`t$(Esc-Xml "$val")"; X "$indent" - } -} - -function Emit-MLText { - param([string]$tag, $text, [string]$indent, [string]$xsiType) - $attr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } - X "$indent<$tag$attr>" - Emit-MLItems -val $text -indent "$indent`t" - X "$indent" -} - -# и подобные DCS-подписи: платформа пишет плоскую строку как -# xsi:type="xs:string" (скаляр; корпус 26), мультиязычный текст — как xsi:type="v8:LocalStringType" -# (7). Декомпилятор различает (Get-PresText: строка ИЛИ объект {ru,en}). -function Emit-USPresentation { - param($val, [string]$tag, [string]$indent) - if ($null -eq $val) { return } - if ($val -is [string]) { - X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)" - } else { - Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType" - } -} - -# Детектор «настоящей» inline-разметки форматированного текста (1С: ///… -# и закрывающий ). Плейсхолдеры вида <не заполнен> НЕ срабатывают (нет известного тега/). -# ВАЖНО: regex должен быть идентичен в form-decompile (иначе гибрид-раундтрип поедет). -$script:fmtMarkupRe = '|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' -function Test-HasRealMarkup { - param($text) - if ($null -eq $text) { return $false } - $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } - elseif ($text -is [System.Management.Automation.PSCustomObject]) { @($text.PSObject.Properties.Value) } - else { @("$text") } - foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } - return $false -} -# DSL-значение ML-поля → @{ text; formatted }. Форма {text, formatted} = явный override; -# строка/мапа → авто-детект formatted по разметке. -function Resolve-MLFormatted { - param($val) - $hasText = $false - if ($val -is [System.Management.Automation.PSCustomObject]) { $hasText = [bool]$val.PSObject.Properties['text'] } - elseif ($val -is [System.Collections.IDictionary]) { $hasText = $val.Contains('text') } - if ($hasText) { - $t = if ($val -is [System.Collections.IDictionary]) { $val['text'] } else { $val.text } - $f = if ($val -is [System.Collections.IDictionary]) { $val['formatted'] } else { $val.formatted } - return @{ text = $t; formatted = [bool]$f } - } - return @{ text = $val; formatted = (Test-HasRealMarkup $val) } -} - -# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). -# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип -# (harness нормализует GUID для хвоста с иными идентификаторами). -$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' -$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' -$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' -$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' - -# ───────────────────────────────────────────────────────────────────────────── -# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. -# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). -# ───────────────────────────────────────────────────────────────────────────── -function New-Guid-String { return [System.Guid]::NewGuid().ToString() } - -$script:comparisonTypes = @{ - "=" = "Equal"; "<>" = "NotEqual" - ">" = "Greater"; ">=" = "GreaterOrEqual" - "<" = "Less"; "<=" = "LessOrEqual" - "in" = "InList"; "notIn" = "NotInList" - "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" - "contains" = "Contains"; "notContains" = "NotContains" - "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" - "like" = "Like"; "notLike" = "NotLike" - "подобно" = "Like"; "неподобно" = "NotLike" # рус. синоним (хэш регистронезависим: ПОДОБНО=подобно) - "filled" = "Filled"; "notFilled" = "NotFilled" -} - -function Parse-FilterShorthand { - param([string]$s) - $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } - if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } - if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } - if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } - if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } - if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible"; $s = $s -replace '\s*@inaccessible', '' } - $s = $s.Trim() - $opPatterns = @('<>', '>=', '<=', '=', '>', '<', - 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', - 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', - 'notLike\b', 'like\b', 'неподобно\b', 'подобно\b', - 'notFilled\b', 'filled\b') - $opJoined = $opPatterns -join '|' - if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { - $result.field = $Matches[1].Trim() - $result.op = $Matches[2].Trim() - $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } - if ($valPart -and $valPart -ne "_") { - if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true"); $result["valueType"] = "xs:boolean" } - elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart } # дата без valueType → Emit-FilterItem выведет StandardBeginningDate Custom (дефолт даты в фильтре) - elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart; $result["valueType"] = "xs:decimal" } - elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart; $result["valueType"] = "dcscor:DesignTimeValue" } - else { $result.value = $valPart; $result["valueType"] = "xs:string" } - } - } else { $result.field = $s } - return $result -} - -function Emit-FilterItem { - param($item, [string]$indent) - if ($item.group) { - $groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } } - X "$indent" - X "$indent`t$groupType" - if ($item.items) { - foreach ($sub in $item.items) { - if ($sub -is [string]) { - $parsed = Parse-FilterShorthand $sub - $obj = @{ field = $parsed.field; op = $parsed.op } - if ($parsed.use -eq $false) { $obj.use = $false } - if ($null -ne $parsed.value) { $obj.value = $parsed.value } - if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } - if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } - if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } - $sub = [pscustomobject]$obj - } - Emit-FilterItem -item $sub -indent "$indent`t" - } - } - if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } - if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } - if ($item.userSettingID) { - $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t$(Esc-Xml $guid)" - } - if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" - return - } - X "$indent" - if ($item.use -eq $false) { X "$indent`tfalse" } - X "$indent`t$(Esc-Xml "$($item.field)")" - $compType = $script:comparisonTypes["$($item.op)"] - if (-not $compType) { $compType = "$($item.op)" } - X "$indent`t$(Esc-Xml $compType)" - $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) - if ($valIsArray) { - if (@($item.value).Count -eq 0) { - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t-1" - X "$indent`t" - } else { - foreach ($v in $item.value) { - $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } - if (-not $vt) { - if ($v -is [bool]) { $vt = 'xs:boolean' } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' } - elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' } - elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' } - elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } - else { $vt = 'xs:string' } - } - $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } - X "$indent`t$vStr" - } - } - } elseif ($null -ne $item.value -and ( - "$($item.valueType)" -match 'Standard(Beginning|End)Date$' -or - (-not $item.valueType -and "$($item.value)" -match '^\d{4}-\d{2}-\d{2}T'))) { - # Стандартная дата начала/окончания. Формы значения: - # объект {variant, date?} — полная (Custom несёт ); - # строка-вариант "BeginningOfThisDay" — именованный вариант без даты; - # голая ISO-дата без valueType — шорткат для Custom+date (дата в фильтре платформой - # почти всегда хранится как StandardBeginningDate Custom, корпус 268 vs 2 xs:dateTime; - # явный valueType="xs:dateTime" → плоская дата, ветка ниже). - $sdType = if ($item.valueType) { "$($item.valueType)" -replace '^v8:','' } else { 'StandardBeginningDate' } - $sv = $item.value - if (($sv -is [PSCustomObject]) -or ($sv -is [System.Collections.IDictionary])) { - $variant = if ($sv -is [PSCustomObject]) { "$($sv.variant)" } else { "$($sv['variant'])" } - $hasDate = if ($sv -is [PSCustomObject]) { [bool]$sv.PSObject.Properties['date'] } else { $sv.Contains('date') } - $dateV = if ($hasDate) { if ($sv -is [PSCustomObject]) { "$($sv.date)" } else { "$($sv['date'])" } } else { $null } - } elseif ("$sv" -match '^\d{4}-\d{2}-\d{2}T') { - $variant = 'Custom'; $hasDate = $true; $dateV = "$sv" - } else { - $variant = "$sv"; $hasDate = $false; $dateV = $null - } - X "$indent`t" - X "$indent`t`t$(Esc-Xml $variant)" - if ($hasDate) { X "$indent`t`t$(Esc-Xml $dateV)" } - X "$indent`t" - } elseif ("$($item.value)" -eq '_') { - # "_" — маркер пустого значения: платформа эмитит пустой self-closing - # (напр. — сравнение с незаданным полем). - $vt = if ($item.valueType) { "$($item.valueType)" } else { 'xs:string' } - X "$indent`t" - } elseif ($null -ne $item.value) { - $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } - if (-not $vt) { - $v = $item.value - if ($v -is [bool]) { $vt = "xs:boolean" } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = "xs:decimal" } - elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = "xs:dateTime" } - elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" } - elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" } - else { $vt = "xs:string" } - } - $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } - X "$indent`t$vStr" - } - if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } - if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } - if ($item.userSettingID) { - $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } - X "$indent`t$(Esc-Xml $uid)" - } - if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } - X "$indent" -} - -function Emit-Filter { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - X "$indent" - foreach ($item in $items) { - if ($item -is [string]) { - $parsed = Parse-FilterShorthand $item - $obj = @{ field = $parsed.field; op = $parsed.op } - if ($parsed.use -eq $false) { $obj.use = $false } - if ($null -ne $parsed.value) { $obj.value = $parsed.value } - if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } - if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } - if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } - Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t" - } else { Emit-FilterItem -item $item -indent "$indent`t" } - } - if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t$(Esc-Xml $uid)" - } - X "$indent" -} - -function Emit-Order { - param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - X "$indent" - foreach ($item in $items) { - if ($item -is [string]) { - if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t" } } - else { - $parts = $item -split '\s+' - $field = $parts[0] - $dir = "Asc" - if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" } - elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" } - X "$indent`t" - X "$indent`t`t$(Esc-Xml $field)" - X "$indent`t`t$dir" - X "$indent`t" - } - } else { - if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t" }; continue } - $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" } - if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" } - X "$indent`t" - if ($item.use -eq $false) { X "$indent`t`tfalse" } - X "$indent`t`t$(Esc-Xml "$($item.field)")" - X "$indent`t`t$dir" - if ($item.viewMode) { X "$indent`t`t$(Esc-Xml "$($item.viewMode)")" } - X "$indent`t" - } - } - if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t$(Esc-Xml $uid)" - } - X "$indent" -} - -function Emit-AppearanceValue { - param([string]$key, $val, [string]$indent) - X "$indent" - function _HasKey { param($o, [string]$k) - if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] } - if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) } - return $false - } - function _Get { param($o, [string]$k) - if ($o -is [PSCustomObject]) { return $o.$k } - if ($o -is [System.Collections.IDictionary]) { return $o[$k] } - return $null - } - $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line') - $useWrapper = $false - $innerVal = $val - $nestedItems = $null - if ($isTopLevelLine) { - if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } - if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } - } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) { - $innerVal = (_Get $val 'value') - if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } - if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } - } - if ($useWrapper) { X "$indent`tfalse" } - X "$indent`t$(Esc-Xml $key)" - $isFontDict = $false - if ($innerVal -is [PSCustomObject]) { - $tProp = $innerVal.PSObject.Properties['@type'] - if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } - } elseif ($innerVal -is [System.Collections.IDictionary]) { - if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true } - } - $isLineDict = $false - if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') } - $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) - if ($isLineDict) { - $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 } - $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } - $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } - X "$indent`t" - X "$indent`t`t$(Esc-Xml $ls)" - X "$indent`t" - } elseif ($isFontDict) { - $attrParts = @() - foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $av = $null - if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } } - else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } } - if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } - } - X "$indent`t" - } elseif ($isDict -and (_HasKey $innerVal 'field')) { - # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки - X "$indent`t$(Esc-Xml "$(_Get $innerVal 'field')")" - } elseif ($isDict) { - # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value - Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType" - } else { - $actualVal = "$innerVal" - $keyTypeMap = @{ - 'Размещение' = 'dcscor:DataCompositionTextPlacementType' - 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign' - 'ВертикальноеПоложение' = 'v8ui:VerticalAlign' - 'ОриентацияТекста' = 'xs:decimal' - 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement' - 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' - } - $keyType = $keyTypeMap[$key] - if ($keyType) { X "$indent`t$(Esc-Xml $actualVal)" } - elseif ($actualVal -match '^(style|web|win):') { X "$indent`t$(Esc-Xml $actualVal)" } - elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t$actualVal" } - elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { - # Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит - # нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше). - # Пустая строка → самозакрывающийся тег (как у платформы). - if ($actualVal -eq '') { X "$indent`t" } - else { X "$indent`t$(Esc-Xml $actualVal)" } - } - elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t$actualVal" } - elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t$(Esc-Xml $actualVal)" } - else { X "$indent`t$(Esc-Xml $actualVal)" } - } - if ($nestedItems) { - $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } - if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } } - elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } } - } - X "$indent" -} - -function Emit-ConditionalAppearance { - param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance') - $hasItems = $items -and $items.Count -gt 0 - $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) - if (-not $hasItems -and -not $hasBlockMeta) { return } - X "$indent<$wrapTag>" - foreach ($ca in $items) { - X "$indent`t" - if ($ca.use -eq $false) { X "$indent`t`tfalse" } - if ($ca.selection -and $ca.selection.Count -gt 0) { - X "$indent`t`t" - foreach ($sel in $ca.selection) { - X "$indent`t`t`t" - X "$indent`t`t`t`t$(Esc-Xml "$sel")" - X "$indent`t`t`t" - } - X "$indent`t`t" - } else { X "$indent`t`t" } - if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" } - else { X "$indent`t`t" } - if ($ca.appearance) { - X "$indent`t`t" - foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" } - X "$indent`t`t" - } - if ($ca.presentation) { - if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { - # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) - X "$indent`t`t" - Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t" - X "$indent`t`t" - } - else { X "$indent`t`t$(Esc-Xml "$($ca.presentation)")" } - } - if ($ca.viewMode) { X "$indent`t`t$(Esc-Xml "$($ca.viewMode)")" } - if ($ca.userSettingID) { - $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } - X "$indent`t`t$(Esc-Xml $uid)" - } - if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } - if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { - $useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader') - $set = @{} - foreach ($n in $ca.useInDontUse) { $set["$n"] = $true } - foreach ($n in $useInOrder) { - if ($set.ContainsKey($n)) { - $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1)) - X "$indent`t`tDontUse" - } - } - } - X "$indent`t" - } - if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } - if ($null -ne $blockUserSettingID) { - $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } - X "$indent`t$(Esc-Xml $uid)" - } - X "$indent" -} - -# --- 5. Type emitter --- - -$script:formTypeSynonyms = New-Object System.Collections.Hashtable -$script:formTypeSynonyms["строка"] = "string" -$script:formTypeSynonyms["число"] = "decimal" -$script:formTypeSynonyms["булево"] = "boolean" -$script:formTypeSynonyms["дата"] = "date" -$script:formTypeSynonyms["датавремя"]= "dateTime" -$script:formTypeSynonyms["number"] = "decimal" -$script:formTypeSynonyms["bool"] = "boolean" -$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" -$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" -$script:formTypeSynonyms["документссылка"] = "DocumentRef" -$script:formTypeSynonyms["документобъект"] = "DocumentObject" -$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" -$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" -$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" -$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" -$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" -$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" -$script:formTypeSynonyms["задачассылка"] = "TaskRef" -$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" -$script:formTypeSynonyms["характеристика"] = "Characteristic" -$script:formTypeSynonyms["любаяссылка"] = "AnyRef" -$script:formTypeSynonyms["любаяссылкаиб"] = "AnyIBRef" -# Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: (эмитим verbatim) -$script:formTypeSynonyms["standardperiod"] = "v8:StandardPeriod" -$script:formTypeSynonyms["стандартныйпериод"] = "v8:StandardPeriod" -$script:formTypeSynonyms["standardbeginningdate"] = "v8:StandardBeginningDate" -$script:formTypeSynonyms["стандартнаядатаначала"] = "v8:StandardBeginningDate" -$script:formTypeSynonyms["uuid"] = "v8:UUID" -$script:formTypeSynonyms["уникальныйидентификатор"] = "v8:UUID" -$script:formTypeSynonyms["списокзначений"] = "ValueList" - -# Known invalid types (runtime/UI types that don't exist in XDTO schema) -$script:knownInvalidTypes = @{ - "FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)" - "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" -} - -function Resolve-TypeStr { - param([string]$typeStr) - if (-not $typeStr) { return $typeStr } - # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) - if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } - if ($typeStr -match '^([^(]+)\((.+)\)$') { - $base = $Matches[1].Trim(); $params = $Matches[2] - $r = $script:formTypeSynonyms[$base.ToLower()] - if ($r) { return "$r($params)" } - return $typeStr - } - if ($typeStr.Contains('.')) { - $i = $typeStr.IndexOf('.') - $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) - $r = $script:formTypeSynonyms[$prefix.ToLower()] - if ($r) { return "$r$suffix" } - return $typeStr - } - $r = $script:formTypeSynonyms[$typeStr.ToLower()] - if ($r) { return $r } - return $typeStr -} - -function Emit-Type { - # $tag/$tagAttrs — обёртка (по умолчанию ); для уточнения типа значений ValueList - # вызывается с tag="Settings", tagAttrs=' xsi:type="v8:TypeDescription"'. - param($typeStr, [string]$indent, [string]$tag = "Type", [string]$tagAttrs = "") - - if (-not $typeStr) { - X "$indent<$tag$tagAttrs/>" - return - } - - $typeString = "$typeStr" - - # Composite type: "Type1 | Type2" or "Type1 + Type2" - $parts = $typeString -split '\s*[|+]\s*' - - X "$indent<$tag$tagAttrs>" - foreach ($part in $parts) { - $part = $part.Trim() - Emit-SingleType -typeStr $part -indent "$indent`t" - } - X "$indent" -} - -function Emit-SingleType { - param([string]$typeStr, [string]$indent) - - $typeStr = Resolve-TypeStr $typeStr - - # boolean - if ($typeStr -eq "boolean") { - X "$indentxs:boolean" - return - } - - # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) - if ($typeStr -match '^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$') { - $len = if ($Matches[2]) { $Matches[2] } else { "0" } - $al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' } - X "$indentxs:string" - X "$indent" - X "$indent`t$len" - X "$indent`t$al" - X "$indent" - return - } - - # decimal(D,F) or decimal(D,F,nonneg) - if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { - $digits = $Matches[1] - $fraction = $Matches[2] - $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } - X "$indentxs:decimal" - X "$indent" - X "$indent`t$digits" - X "$indent`t$fraction" - X "$indent`t$sign" - X "$indent" - return - } - - # date / dateTime / time - if ($typeStr -match '^(date|dateTime|time)$') { - $fractions = switch ($typeStr) { - "date" { "Date" } - "dateTime" { "DateTime" } - "time" { "Time" } - } - X "$indentxs:dateTime" - X "$indent" - X "$indent`t$fractions" - X "$indent" - return - } - - # ValueTable, ValueTree, ValueList, etc. - $v8Types = @{ - "ValueTable" = "v8:ValueTable" - "ValueTree" = "v8:ValueTree" - "ValueList" = "v8:ValueListType" - "TypeDescription" = "v8:TypeDescription" - "Universal" = "v8:Universal" - "FixedArray" = "v8:FixedArray" - "FixedStructure" = "v8:FixedStructure" - } - if ($v8Types.ContainsKey($typeStr)) { - X "$indent$($v8Types[$typeStr])" - return - } - - # UI types - $uiTypes = @{ - "FormattedString" = "v8ui:FormattedString" - "Picture" = "v8ui:Picture" - "Color" = "v8ui:Color" - "Font" = "v8ui:Font" - } - if ($uiTypes.ContainsKey($typeStr)) { - X "$indent$($uiTypes[$typeStr])" - return - } - - # DCS types - if ($typeStr -match '^DataComposition') { - $dcsMap = @{ - "DataCompositionSettings" = "dcsset:DataCompositionSettings" - "DataCompositionSchema" = "dcssch:DataCompositionSchema" - "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" - } - if ($dcsMap.ContainsKey($typeStr)) { - X "$indent$($dcsMap[$typeStr])" - return - } - } - - # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. - # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. (Дотированные формы - # ConstantsSet.X / ReportObject.X ловит общий cfg:-regex ниже.) - if ($typeStr -in @("DynamicList","ConstantsSet","ReportObject")) { - X "$indentcfg:$typeStr" - return - } - - # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) - # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. - if ($typeStr -match '^(DefinedType|Characteristic)\.') { - X "$indentcfg:$typeStr" - return - } - if ($typeStr -match '^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$') { - X "$indentcfg:$typeStr" - return - } - - # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) - if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') { - X "$indentcfg:$typeStr" - return - } - - # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). - # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. - # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, - # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. - $specialTypeNs = @{ - "mxl:SpreadsheetDocument" = "http://v8.1c.ru/8.2/data/spreadsheet" - "fd:FormattedDocument" = "http://v8.1c.ru/8.2/data/formatted-document" - "d5p1:TextDocument" = "http://v8.1c.ru/8.1/data/txtedt" - "d5p1:Chart" = "http://v8.1c.ru/8.2/data/chart" - "d5p1:GanttChart" = "http://v8.1c.ru/8.2/data/chart" - "d5p1:Dendrogram" = "http://v8.1c.ru/8.2/data/chart" - "d5p1:FlowchartContextType" = "http://v8.1c.ru/8.2/data/graphscheme" - "d5p1:DataAnalysisTimeIntervalUnitType" = "http://v8.1c.ru/8.2/data/data-analysis" - "d5p1:GeographicalSchema" = "http://v8.1c.ru/8.2/data/geo" - "pdfdoc:PDFDocument" = "http://v8.1c.ru/8.3/data/pdf" - "pl:Planner" = "http://v8.1c.ru/8.3/data/planner" - } - if ($specialTypeNs.ContainsKey($typeStr)) { - $pref = $typeStr.Substring(0, $typeStr.IndexOf(':')) - X "$indent$typeStr" - return - } - - # Fallback with validation - if ($script:knownInvalidTypes.ContainsKey($typeStr)) { - throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" - } - # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — эмитим verbatim (напр. v8:UUID, v8:StandardPeriod). - if ($typeStr -match '^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):') { - X "$indent$typeStr" - } elseif ($typeStr.Contains('.')) { - X "$indentcfg:$typeStr" - } else { - Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" - X "$indent$typeStr" - } -} - -# --- 6. Event handler name generator --- - -$script:eventSuffixMap = @{ - "OnChange" = "ПриИзменении" - "StartChoice" = "НачалоВыбора" - "ChoiceProcessing" = "ОбработкаВыбора" - "AutoComplete" = "АвтоПодбор" - "Clearing" = "Очистка" - "Opening" = "Открытие" - "Click" = "Нажатие" - "OnActivateRow" = "ПриАктивизацииСтроки" - "BeforeAddRow" = "ПередНачаломДобавления" - "BeforeDeleteRow" = "ПередУдалением" - "BeforeRowChange" = "ПередНачаломИзменения" - "OnStartEdit" = "ПриНачалеРедактирования" - "OnEndEdit" = "ПриОкончанииРедактирования" - "Selection" = "ВыборСтроки" - "OnCurrentPageChange" = "ПриСменеСтраницы" - "TextEditEnd" = "ОкончаниеВводаТекста" - "URLProcessing" = "ОбработкаНавигационнойСсылки" - "DragStart" = "НачалоПеретаскивания" - "Drag" = "Перетаскивание" - "DragCheck" = "ПроверкаПеретаскивания" - "Drop" = "Помещение" - "AfterDeleteRow" = "ПослеУдаления" -} - -function Get-HandlerName { - param([string]$elementName, [string]$eventName) - $suffix = $script:eventSuffixMap[$eventName] - if ($suffix) { - return "$elementName$suffix" - } - return "$elementName$eventName" -} - -# --- 7. Element emitters --- - -function Get-ElementName { - param($el, [string]$typeKey) - if ($el.name) { return "$($el.name)" } - return "$($el.$typeKey)" -} - -$script:knownEvents = @{ - "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") - "check" = @("OnChange") - "radio" = @("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" = @() -} -$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") - -# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. -# Основной формат: $el.events = { Событие: ИмяОбработчика } (null/"" → авто-имя по конвенции). -# Legacy (принимается ради совместимости): $el.on (массив) + $el.handlers (переопределение имён). -function Get-EventPairs { - param($el, [string]$elementName) - $pairs = New-Object System.Collections.ArrayList - if ($el.events) { - foreach ($p in $el.events.PSObject.Properties) { - $h = "$($p.Value)" - if ([string]::IsNullOrEmpty($h)) { $h = Get-HandlerName -elementName $elementName -eventName $p.Name } - [void]$pairs.Add([pscustomobject]@{ name = $p.Name; handler = $h }) - } - } elseif ($el.on) { - foreach ($evt in $el.on) { - $evtName = "$evt" - $h = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } else { Get-HandlerName -elementName $elementName -eventName $evtName } - [void]$pairs.Add([pscustomobject]@{ name = $evtName; handler = $h }) - } - } - return $pairs -} - -# Проверить, подключено ли событие к элементу (в любом из форматов). -function Test-ElementEvent { - param($el, [string]$eventName) - if ($el.events) { - foreach ($p in $el.events.PSObject.Properties) { if ($p.Name -eq $eventName) { return $true } } - } - if ($el.on -contains $eventName) { return $true } - return $false -} - -function Emit-Events { - param($el, [string]$elementName, [string]$indent, [string]$typeKey) - - $pairs = Get-EventPairs -el $el -elementName $elementName - if ($pairs.Count -eq 0) { return } - - # Validate event names - if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { - $allowed = $script:knownEvents[$typeKey] - foreach ($pr in $pairs) { - if ($allowed.Count -gt 0 -and $allowed -notcontains "$($pr.name)") { - Write-Host "[WARN] Unknown event '$($pr.name)' for $typeKey '$elementName'. Known: $($allowed -join ', ')" - } - } - } - - X "$indent" - foreach ($pr in $pairs) { - X "$indent`t$($pr.handler)" - } - X "$indent" -} - -# ExtendedTooltip — это LabelDecoration: может нести own-content (layout/оформление/флаги/hyperlink) -# вместо/вместе с текстом. Признак структурированной формы: объект с любым НЕ-текстовым ключом -# ({text,formatted} и языковые ключи {ru,en} → обычная текст-форма). -$script:companionStructKeys = @( - 'width','autoMaxWidth','maxWidth','height','autoMaxHeight','maxHeight','verticalAlign','titleHeight', - 'horizontalStretch','verticalStretch','horizontalAlign','groupHorizontalAlign','groupVerticalAlign', - 'visible','hidden','enabled','disabled','hyperlink','events', - 'textColor','backColor','borderColor','font','border','цветтекста','цветфона','цветрамки','шрифт','рамка' -) -function Test-CompanionStructured { - param($content) - if (-not (($content -is [System.Collections.IDictionary]) -or ($content -is [System.Management.Automation.PSCustomObject]))) { return $false } - foreach ($k in $script:companionStructKeys) { - $present = if ($content -is [System.Collections.IDictionary]) { $content.Contains($k) } else { [bool]$content.PSObject.Properties[$k] } - if ($present) { return $true } - } - return $false -} - -function Emit-CompanionTitle { - param($content, [string]$indent) - $r = Resolve-MLFormatted $content - $fmt = if ($r.formatted) { 'true' } else { 'false' } - X "$indent" - Emit-MLItems -val $r.text -indent "$indent`t" - X "$indent" -} - -# DisplayImportance — атрибут открывающего тега элемента (адаптивная важность: VeryHigh/High/Usual/Low/VeryLow). -# Возвращает ` DisplayImportance="X"` или "" (для companion-эмиттеров без $el → "" молча). -function DI-Attr { - param($el) - if ($null -ne $el -and $el.displayImportance) { return " DisplayImportance=`"$(Esc-Xml "$($el.displayImportance)")`"" } - return "" -} - -function Emit-Companion { - param([string]$tag, [string]$name, [string]$indent, $content = $null) - $id = New-Id - $hasContent = $null -ne $content -and -not ($content -is [string] -and "$content" -eq '') - if (-not $hasContent) { - X "$indent<$tag name=`"$name`" id=`"$id`"/>" - return - } - $inner = "$indent`t" - X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" - if (Test-CompanionStructured $content) { - # структурированная форма (own-content). Порядок как у платформы: own-content (флаги/hyperlink/ - # layout/оформление) ПЕРЕД Title (в корпусе layout-first 582 vs 10). - $txtPresent = if ($content -is [System.Collections.IDictionary]) { $content.Contains('text') } else { [bool]$content.PSObject.Properties['text'] } - Emit-CommonFlags -el $content -indent $inner - if ($content.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $content -indent $inner - Emit-Appearance -el $content -indent $inner -profile 'decoration' - if ($txtPresent) { Emit-CompanionTitle -content $content -indent $inner } - # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) - Emit-Events -el $content -elementName $name -indent $inner -typeKey 'label' - } else { - Emit-CompanionTitle -content $content -indent $inner - } - X "$indent" -} - -# Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, children?[] } -# или массив = shorthand для { children }. Пусто/нет → self-closing companion (как Emit-Companion). -# Дети — обычная грамматика button/buttonGroup/popup (Emit-Element, inCmdBar). -function Emit-CompanionPanel { - param([string]$tag, [string]$name, [string]$indent, $panel) - $id = New-Id - $autofill = $null - $children = $null - $halign = $null - if ($panel -is [array]) { - $children = $panel - } elseif ($null -ne $panel) { - if ($null -ne $panel.PSObject.Properties['autofill'] -and $null -ne $panel.autofill) { $autofill = [bool]$panel.autofill } - if ($null -ne $panel.PSObject.Properties['horizontalAlign'] -and "$($panel.horizontalAlign)" -ne '') { $halign = "$($panel.horizontalAlign)" } - $children = $panel.children - } - $hasChildren = $children -and @($children).Count -gt 0 - # Платформа пишет только при false; true = дефолт (тег опускается). - $emitAfFalse = ($autofill -eq $false) - if (-not $emitAfFalse -and -not $hasChildren -and -not $halign) { - X "$indent<$tag name=`"$name`" id=`"$id`"/>" - return - } - X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" - if ($halign) { X "$indent`t$halign" } - if ($emitAfFalse) { X "$indent`tfalse" } - if ($hasChildren) { - X "$indent`t" - foreach ($c in @($children)) { Emit-Element -el $c -indent "$indent`t`t" -inCmdBar $true } - X "$indent`t" - } - X "$indent" -} - -# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type. -$script:additionTypeMap = [ordered]@{ - 'searchString' = @{ Tag = 'SearchStringAddition'; Type = 'SearchStringRepresentation'; Suffix = 'СтрокаПоиска' } - 'viewStatus' = @{ Tag = 'ViewStatusAddition'; Type = 'ViewStatusRepresentation'; Suffix = 'СостояниеПросмотра' } - 'searchControl' = @{ Tag = 'SearchControlAddition'; Type = 'SearchControl'; Suffix = 'УправлениеПоиском' } -} -# Синонимы типа дополнения (для override-карты additions и резолва тип-ключа). -$script:additionKeySynonyms = @{ - 'searchString' = @('SearchStringAddition','SearchStringRepresentation','строкаПоиска','отображениеСтрокиПоиска') - 'viewStatus' = @('ViewStatusAddition','ViewStatusRepresentation','состояниеПросмотра') - 'searchControl' = @('SearchControlAddition','SearchControl','управлениеПоиском') -} - -# HorizontalLocation: auto (дефолт, тег опускаем) / left / right; forgiving + рус.синонимы. -function Get-HLocation { - param($el) - $v = if ($el -and $el.PSObject.Properties['horizontalLocation']) { $el.horizontalLocation } else { $null } - if (-not $v) { return $null } - switch -Regex ("$v".ToLower()) { - '^(auto|авто)$' { return $null } # дефолт — не эмитим - '^(left|слева|лево)$' { return 'Left' } - '^(right|справа|право)$' { return 'Right' } - '^(center|центр|по центру)$' { return 'Center' } - default { return "$v" } - } -} - -# Тело дополнения: AdditionSource + свойства (как у поля) + companions. $props может быть $null -# (стандартное дополнение без отклонений). Порядок 1С-толерантен (diff порядок-независим). -function Emit-AdditionBody { - param($props, [string]$source, [string]$srcType, [string]$addName, [string]$indent) - $inner = "$indent`t" - X "$inner" - X "$inner`t$source" - X "$inner`t$srcType" - X "$inner" - if ($props) { - if ($props.PSObject.Properties['title'] -and $props.title) { Emit-MLText -tag "Title" -text $props.title -indent $inner } - Emit-CommonFlags -el $props -indent $inner - if ($props.tooltip) { Emit-MLText -tag "ToolTip" -text $props.tooltip -indent $inner } - if ($props.tooltipRepresentation) { X "$inner$($props.tooltipRepresentation)" } - $hl = Get-HLocation $props; if ($hl) { X "$inner$hl" } - Emit-Layout -el $props -indent $inner - Emit-Appearance -el $props -indent $inner -profile 'field' - } - Emit-Companion -tag "ContextMenu" -name "${addName}КонтекстноеМеню" -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${addName}РасширеннаяПодсказка" -indent $inner -} - -# Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. -function Emit-Addition { - param($el, [string]$name, [int]$id, [string]$typeKey, [string]$indent) - $map = $script:additionTypeMap[$typeKey] - $source = if ($el.source) { "$($el.source)" } elseif ($script:currentTableName) { $script:currentTableName } else { '' } - X "$indent<$($map.Tag) name=`"$name`" id=`"$id`"$(DI-Attr $el)>" - Emit-AdditionBody -props $el -source $source -srcType $map.Type -addName $name -indent $indent - X "$indent" -} - -# Стандартное табличное дополнение (авто-генерация на уровне таблицы). $override — объект отклонений -# из per-table карты additions (или $null = чистый дефолт). -function Emit-TableAddition { - param([string]$typeKey, [string]$tableName, [string]$indent, $override = $null) - $map = $script:additionTypeMap[$typeKey] - $addName = "$tableName$($map.Suffix)" - $id = New-Id - X "$indent<$($map.Tag) name=`"$addName`" id=`"$id`">" - Emit-AdditionBody -props $override -source $tableName -srcType $map.Type -addName $addName -indent $indent - X "$indent" -} - -# Прочитать override-объект для типа дополнения из per-table карты additions (с синонимами). -function Get-AdditionOverride { - param($additions, [string]$typeKey) - if ($null -eq $additions) { return $null } - foreach ($k in @($typeKey) + $script:additionKeySynonyms[$typeKey]) { - $p = $additions.PSObject.Properties[$k] - if ($p) { return $p.Value } - } - return $null -} - -function Emit-Element { - param($el, [string]$indent, [bool]$inCmdBar = $false) - - # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. - Normalize-PanelSynonyms $el - - # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). - # Maps any synonym to canonical short DSL key. - # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя); - # объект/массив уже отнесён к панель-свойству выше. - $strOnlyKeys = @('commandBar','autoCommandBar','КоманднаяПанель') - $synonyms = @{ - "commandBar" = "cmdBar" - "autoCommandBar" = "autoCmdBar" - "КоманднаяПанель" = "cmdBar" - "InputField" = "input" - "ПолеВвода" = "input" - "CheckBoxField" = "check" - "ПолеФлажка" = "check" - "RadioButtonField" = "radio" - "ПолеПереключателя" = "radio" - "radioButton" = "radio" - "PictureField" = "picField" - "ПолеКартинки" = "picField" - "LabelField" = "labelField" - "ПолеНадписи" = "labelField" - "CalendarField" = "calendar" - "ПолеКалендаря" = "calendar" - "LabelDecoration" = "label" - "Надпись" = "label" - "PictureDecoration" = "picture" - "Картинка" = "picture" - "UsualGroup" = "group" - "Группа" = "group" - "ОбычнаяГруппа" = "group" - "ColumnGroup" = "columnGroup" - "ГруппаКолонок" = "columnGroup" - "Pages" = "pages" - "ГруппаСтраниц" = "pages" - "Page" = "page" - "Страница" = "page" - "Table" = "table" - "Таблица" = "table" - "Button" = "button" - "Кнопка" = "button" - "Popup" = "popup" - "ВсплывающееМеню" = "popup" - # Дополнения командной панели таблицы (тип-как-ключ) — forgiving: XML-тег/Type/рус.имя → канон - "SearchStringAddition" = "searchString" - "SearchStringRepresentation" = "searchString" - "строкаПоиска" = "searchString" - "отображениеСтрокиПоиска" = "searchString" - "Отображение строки поиска" = "searchString" - "ViewStatusAddition" = "viewStatus" - "ViewStatusRepresentation" = "viewStatus" - "состояниеПросмотра" = "viewStatus" - "Состояние просмотра" = "viewStatus" - "SearchControlAddition" = "searchControl" - "SearchControl" = "searchControl" - "управлениеПоиском" = "searchControl" - "Управление поиском" = "searchControl" - # Спец-поля (документ/датчик) — XML-имя/рус. → канон - "SpreadSheetDocumentField" = "spreadsheet" - "ПолеТабличногоДокумента" = "spreadsheet" - "HTMLDocumentField" = "html" - "ПолеHTMLДокумента" = "html" - "TextDocumentField" = "textDoc" - "ПолеТекстовогоДокумента" = "textDoc" - "FormattedDocumentField" = "formattedDoc" - "ПолеФорматированногоДокумента" = "formattedDoc" - "ProgressBarField" = "progressBar" - "ПолеИндикатора" = "progressBar" - "TrackBarField" = "trackBar" - "ПолеПолосыРегулирования" = "trackBar" - "ChartField" = "chart" - "ПолеДиаграммы" = "chart" - "GanttChartField" = "ganttChart" - "ПолеДиаграммыГанта" = "ganttChart" - "GraphicalSchemaField" = "graphicalSchema" - "ПолеГрафическойСхемы" = "graphicalSchema" - "PlannerField" = "planner" - "ПолеПланировщика" = "planner" - "PeriodField" = "periodField" - "ПолеПериода" = "periodField" - "DendrogramField" = "dendrogram" - "ПолеДендрограммы" = "dendrogram" - } - foreach ($pair in $synonyms.GetEnumerator()) { - if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { - if ($strOnlyKeys -contains $pair.Key -and -not ($el.($pair.Key) -is [string])) { continue } - $val = $el.($pair.Key) - $el.PSObject.Properties.Remove($pair.Key) | Out-Null - $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force - } - } - - # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. - # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. - foreach ($pn in @($el.PSObject.Properties.Name)) { - $norm = ($pn -replace '\s','').ToLower() - $canon = $script:propSynonyms[$norm] - if ($canon -and $pn -ne $canon) { - if ($null -eq $el.PSObject.Properties[$canon]) { - $val = $el.($pn) - $el | Add-Member -NotePropertyName $canon -NotePropertyValue $val -Force - } - $el.PSObject.Properties.Remove($pn) | Out-Null - } - } - - # Determine element type from key - $typeKey = $null - $xmlTag = $null - - # picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка - # у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. - # pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей - # (Horizontal), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. - foreach ($key in @("columnGroup","buttonGroup","pages","page","group","input","check","radio","label","labelField","table","button","calendar","cmdBar","popup","searchString","viewStatus","searchControl","picField","picture","spreadsheet","html","textDoc","formattedDoc","progressBar","trackBar","chart","ganttChart","graphicalSchema","planner","periodField","dendrogram")) { - if ($el.$key -ne $null) { - $typeKey = $key - break - } - } - - if (-not $typeKey) { - Write-Warning "Unknown element type, skipping" - return - } - - # Validate known keys — warn about typos and unknown properties - $knownKeys = @{ - # type keys - "group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 - "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 - # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры - "spreadsheet"=1;"html"=1;"textDoc"=1;"formattedDoc"=1;"progressBar"=1;"trackBar"=1 - "chart"=1;"ganttChart"=1;"graphicalSchema"=1;"planner"=1;"periodField"=1;"dendrogram"=1;"ganttTable"=1 - "showPercent"=1;"largeStep"=1;"markingStep"=1;"step"=1 - "horizontalScrollBar"=1;"viewScalingMode"=1;"output"=1;"selectionShowMode"=1;"protection"=1 - "edit"=1;"showGrid"=1;"showGroups"=1;"showHeaders"=1;"showRowAndColumnNames"=1;"showCellNames"=1 - "pointerType"=1;"drawingSelectionShowMode"=1;"warningOnEditRepresentation"=1;"markingAppearance"=1 - # report-form контекст (generic-скаляры элементов) - "horizontalSpacing"=1;"representationInContextMenu"=1;"settingsNamedItemDetailedRepresentation"=1 - # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель - "itemHeight"=1;"dropListWidth"=1;"choiceButtonPicture"=1;"transparentPixel"=1 - # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы - "titleDataPath"=1;"extendedEdit"=1;"maxRowsCount"=1;"autoMaxRowsCount"=1;"heightControlVariant"=1 - "warningOnEdit"=1;"nonselectedPictureText"=1;"editTextUpdate"=1;"footerText"=1 - # columnGroup-specific - "showInHeader"=1 - # radio-specific - "radioButtonType"=1;"choiceList"=1;"columnsCount"=1;"checkBoxType"=1;"editMode"=1 - # naming & binding - "name"=1;"path"=1;"title"=1;"tooltip"=1;"tooltipRepresentation"=1;"extendedTooltip"=1 - # companion-панели (свойства): командная панель + контекстное меню - "commandBar"=1;"contextMenu"=1 - # источник команд группы/панели (ButtonGroup/CommandBar) - "commandSource"=1 - # visibility & state - "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 - # events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости) - "events"=1;"on"=1;"handlers"=1 - # layout - "titleLocation"=1;"representation"=1;"width"=1;"height"=1 - "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 - "maxWidth"=1;"maxHeight"=1 - "groupHorizontalAlign"=1;"groupVerticalAlign"=1;"horizontalAlign"=1 - # input-specific - "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 - "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 - "textEdit"=1 - "wrap"=1;"openButton"=1;"listChoiceMode"=1;"showInFooter"=1 - "extendedEditMultipleValues"=1;"chooseType"=1;"autoCellHeight"=1 - "choiceButtonRepresentation"=1;"footerHorizontalAlign"=1;"headerHorizontalAlign"=1 - "format"=1;"editFormat"=1;"choiceParameters"=1;"choiceParameterLinks"=1;"typeLink"=1 - # label/hyperlink - "hyperlink"=1;"formatted"=1 - # group-specific - "collapsedTitle"=1;"showTitle"=1;"united"=1;"collapsed"=1;"behavior"=1 - # hierarchy - "children"=1;"columns"=1 - # table-specific - "changeRowSet"=1;"changeRowOrder"=1;"autoInsertNewRow"=1;"rowFilter"=1;"header"=1;"footer"=1 - "commandBarLocation"=1;"searchStringLocation"=1;"viewStatusLocation"=1;"searchControlLocation"=1 - "excludedCommands"=1 - "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 - "rowPictureDataPath"=1;"tableAutofill"=1;"heightInTableRows"=1 - "multipleChoice"=1;"searchOnInput"=1;"shortcut"=1 - "rowSelectionMode"=1;"verticalLines"=1;"horizontalLines"=1 - # dynamic-list table block - "defaultItem"=1;"useAlternationRowColor"=1;"fileDragMode"=1;"autoRefresh"=1 - "autoRefreshPeriod"=1;"choiceFoldersAndItems"=1;"restoreCurrentRow"=1;"showRoot"=1 - "allowRootChoice"=1;"updateOnDataChange"=1;"allowGettingCurrentRowURL"=1 - "userSettingsGroup"=1;"rowsPicture"=1 - # calendar-specific - "selectionMode"=1;"showCurrentDate"=1;"widthInMonths"=1;"heightInMonths"=1;"showMonthsPanel"=1 - # pages-specific - "pagesRepresentation"=1 - # button-specific - "type"=1;"command"=1;"commandName"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1;"displayImportance"=1 - # picture/decoration - "src"=1;"valuesPicture"=1;"loadTransparent"=1;"headerPicture"=1;"footerPicture"=1 - # cmdBar-specific - "autofill"=1 - # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице - "autoCmdBar"=1 - # дополнения командной панели таблицы (тип-ключи + свойства) - "searchString"=1;"viewStatus"=1;"searchControl"=1;"source"=1;"horizontalLocation"=1;"additions"=1 - # generic-скаляры (pass-through) + точечные - "verticalAlign"=1;"throughAlign"=1;"enableContentChange"=1;"pictureSize"=1;"titleHeight"=1 - "childItemsWidth"=1;"showLeftMargin"=1;"cellHyperlink"=1;"viewMode"=1;"verticalScrollBar"=1 - "rowInputMode"=1;"mask"=1;"createButton"=1;"fixingInTable"=1;"verticalSpacing"=1 - # InputField choice-скаляры - "choiceListButton"=1;"quickChoice"=1;"autoChoiceIncomplete"=1 - "choiceForm"=1;"choiceHistoryOnInput"=1;"footerDataPath"=1;"minValue"=1;"maxValue"=1 - # Button — пометка toggle-кнопки (ключ 'checked', не 'check' — во избежание конфликта с типом) - "checked"=1 - } - # Оформление (цвета/шрифты/граница) — авто-регистрация из самих структур, чтобы allowlist - # не дрейфовал при добавлении новых ключей/синонимов. Канонические + forgiving-синонимы. - foreach ($k in $script:appearanceSpec.Keys) { $knownKeys[$k] = 1 } - foreach ($k in $script:appearanceSynonyms.Keys) { $knownKeys[$k] = 1 } - foreach ($k in $script:propSynonyms.Keys) { $knownKeys[$k] = 1 } - foreach ($p in $el.PSObject.Properties) { - if ($p.Name -like '_*') { continue } # внутренние маркеры (напр. _dynList) - if (-not $knownKeys.ContainsKey($p.Name)) { - Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." - } - } - - $name = Get-ElementName -el $el -typeKey $typeKey - Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element' - $id = New-Id - - switch ($typeKey) { - "group" { Emit-Group -el $el -name $name -id $id -indent $indent } - "columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent } - "buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent } - "input" { Emit-Input -el $el -name $name -id $id -indent $indent } - "check" { Emit-Check -el $el -name $name -id $id -indent $indent } - "radio" { Emit-Radio -el $el -name $name -id $id -indent $indent } - "label" { Emit-Label -el $el -name $name -id $id -indent $indent } - "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } - "table" { Emit-Table -el $el -name $name -id $id -indent $indent } - "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } - "page" { Emit-Page -el $el -name $name -id $id -indent $indent } - "button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar } - "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } - "searchString" { Emit-Addition -el $el -name $name -typeKey "searchString" -id $id -indent $indent } - "viewStatus" { Emit-Addition -el $el -name $name -typeKey "viewStatus" -id $id -indent $indent } - "searchControl" { Emit-Addition -el $el -name $name -typeKey "searchControl" -id $id -indent $indent } - "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } - "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } - "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } - "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } - "spreadsheet" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "SpreadSheetDocumentField" -typeKey "spreadsheet" } - "html" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "HTMLDocumentField" -typeKey "html" } - "textDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TextDocumentField" -typeKey "textDoc" } - "formattedDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "FormattedDocumentField" -typeKey "formattedDoc" } - "progressBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ProgressBarField" -typeKey "progressBar" } - "trackBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TrackBarField" -typeKey "trackBar" } - "chart" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ChartField" -typeKey "chart" } - "graphicalSchema" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "GraphicalSchemaField" -typeKey "graphicalSchema" } - "planner" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PlannerField" -typeKey "planner" } - "periodField" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PeriodField" -typeKey "periodField" } - "dendrogram" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "DendrogramField" -typeKey "dendrogram" } - "ganttChart" { Emit-GanttChart -el $el -name $name -id $id -indent $indent } - } -} - -# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). -# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). -# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. -# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. -function Emit-XrFlag { - param([string]$tag, $val, [string]$indent) - if ($null -eq $val) { return } - if ($val -is [bool]) { - X "$indent<$tag>" - X "$indent`t$(if ($val){'true'}else{'false'})" - X "$indent" - return - } - # объектная форма { common, roles } - $common = if ($null -ne $val.common) { [bool]$val.common } else { $false } - X "$indent<$tag>" - X "$indent`t$(if ($common){'true'}else{'false'})" - if ($val.roles) { - foreach ($r in $val.roles.PSObject.Properties) { - # Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". - # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. - $rname = "$($r.Name)" -replace '^(Role|Роль)\.', '' - if ($rname -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $rname = "Role.$rname" } - $rval = if ([bool]$r.Value) { 'true' } else { 'false' } - X "$indent`t$rval" - } - } - X "$indent" -} - -function Emit-CommonFlags { - param($el, [string]$indent) - if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } - if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent } - if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } - if ($el.readOnly -eq $true) { X "$indenttrue" } -} - -# Общие layout-свойства — применимы ко всем элементам. Порядок согласован с -# историческим выводом input/label, чтобы не сдвигать существующие снапшоты. -# -skipHeight: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). -# -multiLineDefault: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. -# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. -function Emit-CommonElementProps { - param($el, [string]$indent) - if ($el.defaultItem -eq $true) { X "$indenttrue" } - if ($el.PSObject.Properties['skipOnInput'] -and $null -ne $el.skipOnInput) { - $siv = if ($el.skipOnInput -eq $true) { 'true' } else { 'false' } - X "$indent$siv" - } - # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) - if ($null -ne $el.enableStartDrag) { X "$indent$(if ($el.enableStartDrag){'true'}else{'false'})" } - if ($el.fileDragMode) { X "$indent$($el.fileDragMode)" } - # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» - foreach ($p in @(@('showInHeader','ShowInHeader'), @('showInFooter','ShowInFooter'), @('autoCellHeight','AutoCellHeight'))) { - if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } - } - if ($el.footerHorizontalAlign) { X "$indent$($el.footerHorizontalAlign)" } - if ($el.headerHorizontalAlign) { X "$indent$($el.headerHorizontalAlign)" } -} - -# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). -# Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). -# Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. -# src с префиксом "abs:" → встроенная картинка ; иначе именованная/стилевая . -function Emit-PictureRef { - param($val, [string]$picTag, [string]$indent) - if (-not $val) { return } - $src = $null; $lt = $false; $tpx = $null - if ($val -is [string]) { $src = $val } - else { $src = $val.src; if ($val.loadTransparent -eq $true) { $lt = $true }; $tpx = $val.transparentPixel } - if (-not $src) { return } - $srcStr = "$src" - X "$indent<$picTag>" - if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } - else { X "$indent`t$(Esc-Xml $srcStr)" } - X "$indent`t$(if ($lt) { 'true' } else { 'false' })" - if ($tpx) { X "$indent`t" } - X "$indent" -} - -# Картинки заголовка/подвала колонки поля — по схеме сразу после , -# перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь). -function Emit-ColumnPics { - param($el, [string]$indent) - Emit-PictureRef -val $el.headerPicture -picTag 'HeaderPicture' -indent $indent - Emit-PictureRef -val $el.footerPicture -picTag 'FooterPicture' -indent $indent -} - -# кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false -# (обратная конвенция относительно header/values-картинок). Прощающий ввод: -# принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель -# опишет картинку объектно по аналогии с headerPicture. $elemLt — legacy -# элемент-уровневый ключ loadTransparent (используется, если в объекте флаг не задан). -function Emit-CommandPicture { - param($pic, $elemLt, [string]$indent) - if (-not $pic) { return } - $src = $null; $lt = $null; $tpx = $null - if ($pic -is [string]) { $src = $pic } - else { $src = $pic.src; if ($null -ne $pic.loadTransparent) { $lt = [bool]$pic.loadTransparent }; $tpx = $pic.transparentPixel } - if (-not $src) { return } - if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt } - $srcStr = "$src" - X "$indent" - if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } - else { X "$indent`t$(Esc-Xml $srcStr)" } - X "$indent`t$(if ($lt -eq $false) { 'false' } else { 'true' })" - if ($tpx) { X "$indent`t" } - X "$indent" -} - -# --- Оформление элемента: цвета / шрифты / граница --- -# Прямые свойства элемента (// + header/footer-варианты у полей). -# Ключи DSL — англ. camelCase 1:1 с тегами; принимаем рус. синонимы (forgiving). -# Значения: цвет — verbatim-строка (style:/web:/sys:/win:/#RRGGBB); шрифт — строка-ref или -# объект-атрибуты; граница — строка-ref или объект {width,style|ref}. Порядок тегов — XSD (профиль). -$script:appearanceSpec = @{ - titleTextColor = @{ tag='TitleTextColor'; kind='color' } - titleBackColor = @{ tag='TitleBackColor'; kind='color' } - titleFont = @{ tag='TitleFont'; kind='font' } - footerTextColor = @{ tag='FooterTextColor'; kind='color' } - footerBackColor = @{ tag='FooterBackColor'; kind='color' } - footerFont = @{ tag='FooterFont'; kind='font' } - textColor = @{ tag='TextColor'; kind='color' } - backColor = @{ tag='BackColor'; kind='color' } - borderColor = @{ tag='BorderColor'; kind='color' } - border = @{ tag='Border'; kind='border'} - font = @{ tag='Font'; kind='font' } -} -# Рус. синоним (lower) → canonical key -$script:appearanceSynonyms = @{ - 'цветтекста'='textColor'; 'цветфона'='backColor'; 'цветрамки'='borderColor' - 'цветтекстазаголовка'='titleTextColor'; 'цветфоназаголовка'='titleBackColor'; 'шрифтзаголовка'='titleFont' - 'цветтекстаподвала'='footerTextColor'; 'цветфонаподвала'='footerBackColor'; 'шрифтподвала'='footerFont' - 'шрифт'='font'; 'рамка'='border' -} -# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. -# Ключи карты нормализованы (lowercase, без пробелов); сопоставление в Normalize-PropSynonyms тоже. -# Прощающий ввод: модель может писать свойство по-русски. Англ. ключ работает всегда (это доп. слой). -# Видимость/Доступность НЕ включаем — наш hidden/disabled инвертирован, был бы баг семантики. -$script:propSynonyms = @{ - 'пометка'='checked' - 'кнопкавыбора'='choiceButton'; 'кнопкаочистки'='clearButton'; 'кнопкарегулирования'='spinButton' - 'кнопкавыпадающегосписка'='dropListButton'; 'кнопкасписковоговыбора'='choiceListButton' - 'кнопкаоткрытия'='openButton'; 'кнопкапоумолчанию'='defaultButton' - 'быстрыйвыбор'='quickChoice'; 'формавыбора'='choiceForm'; 'историявыборапривводе'='choiceHistoryOnInput' - 'выборгруппиэлементов'='choiceFoldersAndItems'; 'фиксациявтаблице'='fixingInTable' - 'путькданнымподвала'='footerDataPath'; 'автоотметканезаполненного'='markIncomplete' - 'многострочныйрежим'='multiLine'; 'режимпароля'='passwordMode'; 'переноспословам'='wrap' - 'расположениезаголовка'='titleLocation'; 'пропускатьпривводе'='skipOnInput' - 'заголовок'='title'; 'ширина'='width'; 'высота'='height'; 'подсказкаввода'='inputHint' -} -# Профили порядка тегов по базовым типам (XSD-последовательность) -$script:appOrderField = @('titleTextColor','titleBackColor','titleFont','footerTextColor','footerBackColor','footerFont','textColor','backColor','borderColor','border','font') -$script:appOrderDecoration = @('textColor','font','backColor','borderColor','border') -$script:appOrderButton = @('textColor','backColor','borderColor','font') - -# Простые скаляры элемента (pass-through: captured/emitted «как есть»). Только НЕ-перекрывающиеся -# теги (не обрабатываемые специфично где-то ещё). kind: bool → true/false; value → строка verbatim. -$script:genericScalars = @( - @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } - @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } - @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } - @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } - @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } - @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } - @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } - @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } - @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } - @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } - @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } - @{ Tag='Mask'; Key='mask'; Kind='value' } - @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } - @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } - @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } - # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through - @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } - @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } - @{ Tag='Output'; Key='output'; Kind='value' } - @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } - @{ Tag='PointerType'; Key='pointerType'; Kind='value' } - @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } - @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } - @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } - @{ Tag='Protection'; Key='protection'; Kind='bool' } - @{ Tag='Edit'; Key='edit'; Kind='bool' } - @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } - @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } - @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } - @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } - @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } - @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } - # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы - @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } - @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } - @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } - # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) - @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } - @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } - # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам - @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } - @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } - @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } - @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } - @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } - @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } - # Корпусный хвост: представление управления свёрткой группы / форма кнопки-попапа / - # авто-добавление незаполненной строки / выделение отрицательных / нач. позиция списка / - # высота списка выбора / три состояния флажка / прокрутка страницы при сжатии - @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } - @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } - @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } - @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } - @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } - @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } - @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } - @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } - # Сочетание клавиш — общее свойство (input/group/radio/page/picField/label/table/check; команда — отд. путь, §7) - @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } - # Батч простых скаляров (input/radio/group/picDecoration/button): режим выбора незаполненного, - # равная ширина колонок, выравнивание детей, масштаб/зум картинки, форма/положение картинки кнопки. - # (Table HeaderHeight/FooterHeight/CurrentRowUse — НЕ здесь, а в Emit-Table: pass-through, - # 1С толерантна к порядку детей Table — в корпусе те же теги встречаются в разных позициях.) - @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } - @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } - @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } - @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } - @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } - @{ Tag='Shape'; Key='shape'; Kind='value' } - @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } - # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) - @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } - @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } -) - -function Emit-GenericScalars { - param($el, [string]$indent) - if ($null -eq $el) { return } - foreach ($s in $script:genericScalars) { - $p = $el.PSObject.Properties[$s.Key] - if (-not $p -or $null -eq $p.Value) { continue } - if ($s.Kind -eq 'bool') { - X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})" - } else { - $v = "$($p.Value)"; if ($v -eq '') { continue } - X "$indent<$($s.Tag)>$(Esc-Xml $v)" - } - } -} - -function Get-AppearanceValue { - param($el, [string]$canonical) - if ($null -eq $el) { return $null } - $p = $el.PSObject.Properties[$canonical] - if ($p) { return $p.Value } - foreach ($syn in $script:appearanceSynonyms.Keys) { - if ($script:appearanceSynonyms[$syn] -eq $canonical) { - $pp = $el.PSObject.Properties[$syn] - if ($pp) { return $pp.Value } - } - } - return $null -} - -# — строка = ref на стиль (kind=StyleItem); объект = атрибуты. -function Emit-FontTag { - param([string]$tag, $val, [string]$indent) - if ($val -is [string]) { - X "$indent<$tag ref=`"$(Esc-Xml $val)`" kind=`"StyleItem`"/>" - return - } - $attrs = @() - foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $pp = $val.PSObject.Properties[$a] - if ($pp -and $null -ne $pp.Value) { - $v = $pp.Value - if ($v -is [bool]) { $v = if ($v) {'true'} else {'false'} } - $attrs += "$a=`"$(Esc-Xml "$v")`"" - } - } - X "$indent<$tag $($attrs -join ' ')/>" -} - -# — строка/{ref} = из стиля (); {width,style} = явная. -function Emit-BorderTag { - param($val, [string]$indent) - if ($val -is [string]) { X "$indent"; return } - $refP = $val.PSObject.Properties['ref'] - if ($refP -and $refP.Value) { X "$indent"; return } - $width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 } - $style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null } - X "$indent" - if ($style) { X "$indent`t$(Esc-Xml $style)" } - X "$indent" -} - -# ───────────────────────────────────────────────────────────────────────────── -# Planner design-time — встроенный конфиг планировщика -# на реквизите planner-типа. Структурный DSL: items[] + appearance/поведение-скаляры + -# timeScale (уровни шкалы времени) + period. Каждое присутствующее поле → каноничный -# порядок; пропущенное → дефолт (планировщик всегда несёт полный блок). Декомпилятор -# делает полный захват → раундтрип бит-в-бит; ручной авторинг может быть кратким. -$script:PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' -$script:CHART_NS = 'http://v8.1c.ru/8.2/data/chart' - -function PL-Get { - param($o, [string]$k, $def = $null) - if ($null -ne $o -and $o.PSObject.Properties[$k] -and $null -ne $o.$k) { return $o.$k } - return $def -} -function PL-Bool { - param($v) - if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } } - elseif ("$v" -eq 'True') { 'true' } - elseif ("$v" -eq 'False') { 'false' } - else { "$v" } -} -function Emit-PlannerColor { - param([string]$tag, $o, [string]$key, [string]$ind) - X "$ind$(Esc-Xml "$(PL-Get $o $key 'auto')")" -} -# /… — пустое → самозакрывающийся тег (как в выгрузке платформы). -function Emit-PlannerText { - param([string]$tag, $v, [string]$ind) - if ([string]::IsNullOrEmpty("$v")) { X "$ind" } - else { X "$ind$(Esc-Xml "$v")" } -} -# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef"; -# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы. -function Test-PlannerRef { - param([string]$v) - return ($v -match '^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' -or ` - $v -match '\.EnumValue\.' -or $v -match 'EmptyRef$' -or ` - $v -match '^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') -} -# — nil (нет значения) / xr:DesignTimeRef (ссылка) / xs:string (строка/прочее). -function Emit-PlannerValue { - param($v, [string]$ind) - if ($null -eq $v -or "$v" -eq '') { X "$ind"; return } - $t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' } - X "$ind$(Esc-Xml "$v")" -} -function Emit-PlannerFont { - param($o, [string]$ind) - $f = PL-Get $o 'font' $null - if ($null -eq $f) { X "$ind"; return } - Emit-FontTag -tag 'pl:font' -val $f -indent $ind -} -function Emit-PlannerBorder { - param($o, [string]$ind, [string]$key = 'border') - $b = PL-Get $o $key $null - $bw = if ($b) { PL-Get $b 'width' 1 } else { 1 } - $bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' } - X "$ind" - X "$ind`t$(Esc-Xml "$bs")" - X "$ind" -} -function Emit-PlannerLevel { - param($lv, [string]$cns, [string]$ind) - $li = "$ind`t" - X "$ind" - X "$li$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")" - X "$li$(PL-Get $lv 'interval' 1)" - X "$li$(PL-Bool (PL-Get $lv 'show' $true))" - $line = PL-Get $lv 'line' $null - $lw = if ($line) { PL-Get $line 'width' 1 } else { 1 } - $lg = if ($line) { PL-Get $line 'gap' $false } else { $false } - $lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' } - X "$li" - X "$li`t$(Esc-Xml "$lst")" - X "$li" - X "$li$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")" - X "$li$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")" - $fmt = PL-Get $lv 'format' $null - if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } } - X "$li" - Emit-MLItems -val $fmt -indent "$li`t" - X "$li" - $labels = PL-Get $lv 'labels' $null - $ticks = if ($labels) { PL-Get $labels 'ticks' 0 } else { 0 } - X "$li" - X "$li`t$ticks" - X "$li" - X "$li$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")" - X "$li$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")" - X "$li$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))" - X "$ind" -} -function Emit-PlannerTimeScale { - param($ts, [string]$ind) - $cns = $script:CHART_NS - $ci = "$ind`t" - X "$ind" - X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")" - $levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() } - if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт - foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci } - $transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false } - X "$ci$(PL-Bool $transp)" - X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")" - X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")" - X "$ci$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })" - X "$ind" -} -function Emit-PlannerItem { - param($it, [string]$ind) - X "$ind" - $ii = "$ind`t" - Emit-PlannerValue (PL-Get $it 'value' $null) $ii - Emit-PlannerText 'text' (PL-Get $it 'text' '') $ii - Emit-PlannerText 'tooltip' (PL-Get $it 'tooltip' '') $ii - X "$ii$(PL-Get $it 'begin' '0001-01-01T00:00:00')" - X "$ii$(PL-Get $it 'end' '0001-01-01T00:00:00')" - Emit-PlannerColor 'borderColor' $it 'borderColor' $ii - Emit-PlannerColor 'backColor' $it 'backColor' $ii - Emit-PlannerColor 'textColor' $it 'textColor' $ii - Emit-PlannerFont $it $ii - X "$ii" - X "$ii$(PL-Get $it 'replacementDate' '0001-01-01T00:00:00')" - X "$ii$(PL-Bool (PL-Get $it 'deleted' $false))" - $id = PL-Get $it 'id' $null - if ($null -eq $id) { $id = [guid]::NewGuid().ToString() } - X "$ii$id" - X "$ii$(PL-Bool (PL-Get $it 'textFormatted' $false))" - Emit-PlannerBorder $it $ii 'border' - X "$ii$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")" - X "$ind" -} -# Элемент измерения ( внутри ) — рекурсивен: может нести вложенные -# элементы (UI: колонка «Элементы» у элемента). Порядок: value, text, цвета, font, -# вложенные элементы, showOnlySubordinatesAreas, textFormatted. -function Emit-PlannerDimElement { - param($el, [string]$ind) - X "$ind" - $ii = "$ind`t" - Emit-PlannerValue (PL-Get $el 'value' $null) $ii - Emit-PlannerText 'text' (PL-Get $el 'text' '') $ii - Emit-PlannerColor 'borderColor' $el 'borderColor' $ii - Emit-PlannerColor 'backColor' $el 'backColor' $ii - Emit-PlannerColor 'textColor' $el 'textColor' $ii - Emit-PlannerFont $el $ii - foreach ($sub in @(PL-Get $el 'elements' @())) { Emit-PlannerDimElement $sub $ii } - X "$ii$(PL-Bool (PL-Get $el 'showOnlySubordinatesAreas' $true))" - X "$ii$(PL-Bool (PL-Get $el 'textFormatted' $false))" - X "$ind" -} -# Измерение планировщика () — объект разреза + его элементы. -function Emit-PlannerDimension { - param($d, [string]$ind) - X "$ind" - $di = "$ind`t" - Emit-PlannerValue (PL-Get $d 'value' $null) $di - Emit-PlannerText 'text' (PL-Get $d 'text' '') $di - Emit-PlannerColor 'borderColor' $d 'borderColor' $di - Emit-PlannerColor 'backColor' $d 'backColor' $di - Emit-PlannerColor 'textColor' $d 'textColor' $di - Emit-PlannerFont $d $di - foreach ($el in @(PL-Get $d 'elements' @())) { Emit-PlannerDimElement $el $di } - X "$di$(PL-Bool (PL-Get $d 'textFormatted' $false))" - X "$ind" -} -function Emit-PlannerSettings { - param($pl, [string]$ind) - X "$ind" - $si = "$ind`t" - foreach ($it in @(PL-Get $pl 'items' @())) { Emit-PlannerItem $it $si } - foreach ($d in @(PL-Get $pl 'dimensions' @())) { Emit-PlannerDimension $d $si } - Emit-PlannerColor 'borderColor' $pl 'borderColor' $si - Emit-PlannerColor 'backColor' $pl 'backColor' $si - Emit-PlannerColor 'textColor' $pl 'textColor' $si - Emit-PlannerColor 'lineColor' $pl 'lineColor' $si - Emit-PlannerFont $pl $si - X "$si$(PL-Get $pl 'beginOfRepresentationPeriod' '0001-01-01T00:00:00')" - X "$si$(PL-Get $pl 'endOfRepresentationPeriod' '0001-01-01T00:00:00')" - X "$si$(PL-Bool (PL-Get $pl 'alignElementsOfTimeScale' $true))" - X "$si$(PL-Bool (PL-Get $pl 'displayTimeScaleWrapHeaders' $true))" - X "$si$(PL-Bool (PL-Get $pl 'displayWrapHeaders' $true))" - $wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null - if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } } - Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si - X "$si$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")" - X "$si$(PL-Get $pl 'periodicVariantRepetition' 1)" - X "$si$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)" - X "$si$(PL-Get $pl 'timeScaleWrapEndIndent' 0)" - Emit-PlannerTimeScale (PL-Get $pl 'timeScale' $null) $si - $period = PL-Get $pl 'period' $null - if ($period) { - X "$si" - X "$si`t$(PL-Get $period 'begin' '0001-01-01T00:00:00')" - X "$si`t$(PL-Get $period 'end' '0001-01-01T00:00:00')" - X "$si" - } - X "$si$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))" - X "$si$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")" - X "$si$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")" - X "$si$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))" - X "$si$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))" - X "$si$(PL-Get $pl 'minColumnWidth' 0)" - X "$si$(PL-Get $pl 'minRowHeight' 0)" - X "$si$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")" - X "$si$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")" - Emit-PlannerBorder $pl $si 'border' - X "$si$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")" - X "$ind" -} - -# ───────────────────────────────────────────────────────────────────────────── -# Chart design-time — генерик-эмиттер (зеркало -# Build-ChartNode декомпилятора). Тип узла → форма XML: ML-поля (по имени), серии -# (массив, повтор тега), line/border/font (по ключам), attrs-узлы (gaugeQualityBands), -# иначе вложенный объект/скаляр. Порядок ключей = порядок эмиссии (раундтрип). -$script:CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } -$script:CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } -$script:CHART_FONT_KEYS = @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale') -function Get-Keys { param($o) if ($o -is [System.Collections.IDictionary]) { return @($o.Keys) } else { return @($o.PSObject.Properties.Name) } } -function Get-Prop { param($o, [string]$k) if ($o -is [System.Collections.IDictionary]) { return $o[$k] } else { $p = $o.PSObject.Properties[$k]; if ($p) { return $p.Value } else { return $null } } } -function Emit-ChartNode { - param([string]$name, $val, [string]$ind) - if ($script:CHART_ML_FIELDS.Contains($name)) { - if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } - X "$ind"; Emit-MLItems -val $val -indent "$ind`t"; X "$ind"; return - } - if (($val -is [System.Collections.IList]) -and ($val -isnot [string])) { - foreach ($e in $val) { Emit-ChartNode $name $e $ind } - return - } - if (($val -is [System.Management.Automation.PSCustomObject]) -or ($val -is [System.Collections.IDictionary])) { - $keys = Get-Keys $val - if ($script:CHART_ATTR_FIELDS.Contains($name)) { - $attrs = @(); foreach ($k in $keys) { $v = Get-Prop $val $k; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$k=`"$(Esc-Xml "$v")`"" } - X "$ind"; return - } - if ($keys -contains 'gap') { - $w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style' - X "$ind" - X "$ind`t$(Esc-Xml "$st")" - X "$ind"; return - } - if (($keys -contains 'style') -and ($keys -contains 'width')) { - $w = Get-Prop $val 'width'; $st = Get-Prop $val 'style' - X "$ind" - X "$ind`t$(Esc-Xml "$st")" - X "$ind"; return - } - $isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } } - if ($isFont) { - $attrs = @(); foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $v = Get-Prop $val $fk; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$fk=`"$(Esc-Xml "$v")`"" } } - X "$ind"; return - } - if (@($keys).Count -eq 0) { X "$ind"; return } - X "$ind" - foreach ($k in $keys) { Emit-ChartNode $k (Get-Prop $val $k) "$ind`t" } - X "$ind" - return - } - if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } - if ($val -is [bool]) { X "$ind$(PL-Bool $val)"; return } - X "$ind$(Esc-Xml "$val")" -} -function Emit-ChartSettings { - param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart') - X "$ind" - foreach ($k in (Get-Keys $chart)) { Emit-ChartNode $k (Get-Prop $chart $k) "$ind`t" } - X "$ind" -} - -function Emit-Appearance { - param($el, [string]$indent, [string]$profile = 'field') - if ($null -eq $el) { return } - $order = switch ($profile) { - 'decoration' { $script:appOrderDecoration } - 'button' { $script:appOrderButton } - default { $script:appOrderField } - } - foreach ($key in $order) { - $val = Get-AppearanceValue -el $el -canonical $key - if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue } - $spec = $script:appearanceSpec[$key] - switch ($spec.kind) { - 'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")" } - 'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent } - 'border' { Emit-BorderTag -val $val -indent $indent } - } - } -} - -function Emit-Layout { - param($el, [string]$indent, [switch]$skipHeight, [bool]$multiLineDefault = $false) - # CommandSet (отключённые команды редактора) — общее свойство поля (input/label/check/ - # spreadsheet/html/formatted/picture); в схеме рано (после TitleLocation, перед скалярами). - if ($el.excludedCommands -and @($el.excludedCommands).Count -gt 0) { - X "$indent" - foreach ($cmd in $el.excludedCommands) { X "$indent`t$cmd" } - X "$indent" - } - Emit-CommonElementProps -el $el -indent $indent - $amwExplicit = ($el.PSObject.Properties.Name -contains 'autoMaxWidth') - if ($amwExplicit) { - if ($el.autoMaxWidth -eq $false) { X "$indentfalse" } - } elseif ($multiLineDefault) { - X "$indentfalse" - } - if ($null -ne $el.maxWidth) { X "$indent$($el.maxWidth)" } - if ($el.autoMaxHeight -eq $false) { X "$indentfalse" } - if ($null -ne $el.maxHeight) { X "$indent$($el.maxHeight)" } - if ($el.width) { X "$indent$($el.width)" } - if (-not $skipHeight -and $el.height) { X "$indent$($el.height)" } - if ($null -ne $el.horizontalStretch) { X "$indent$(if ($el.horizontalStretch){'true'}else{'false'})" } - if ($null -ne $el.verticalStretch) { X "$indent$(if ($el.verticalStretch){'true'}else{'false'})" } - if ($el.groupHorizontalAlign) { X "$indent$($el.groupHorizontalAlign)" } - if ($el.groupVerticalAlign) { X "$indent$($el.groupVerticalAlign)" } - if ($el.horizontalAlign) { X "$indent$($el.horizontalAlign)" } - Emit-GenericScalars -el $el -indent $indent -} - -function Title-FromName { - param([string]$name) - if (-not $name) { return '' } - $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') - $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') - $parts = $s -split ' ' - if ($parts.Count -eq 0) { return $s } - $out = New-Object System.Collections.ArrayList - [void]$out.Add($parts[0]) - for ($i = 1; $i -lt $parts.Count; $i++) { - $p = $parts[$i] - if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { - [void]$out.Add($p) - } else { - [void]$out.Add($p.ToLower()) - } - } - return ($out -join ' ') -} - -function Emit-Title { - # Нет ключа title → авто-вывод из имени (помощь модели). - # Явный title: "" (или null) → подавить (заголовок не эмитим). - # Явный непустой → эмитим как есть. - param($el, [string]$name, [string]$indent, [switch]$auto) - $hasKey = $null -ne $el.PSObject.Properties['title'] - if ($hasKey) { - if ($el.title) { Emit-MLText -tag "Title" -text $el.title -indent $indent } - } elseif ($auto -and $name) { - Emit-MLText -tag "Title" -text (Title-FromName -name $name) -indent $indent - } - # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. - if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } - # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. - if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } -} - -function Map-TitleLoc { - param([string]$v) - switch ("$v".ToLower()) { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - "auto" { "Auto" } - default { "$v" } - } -} - -# TitleLocation у check/radio: нет ключа → умный дефолт (Right/None), эмитится; -# "" → подавить (= дефолт платформы, она его сама не пишет); значение → эмитить (маппинг регистра). -function Emit-TitleLocation { - param($el, [string]$indent, [string]$smartDefault) - if ($null -ne $el.PSObject.Properties['titleLocation']) { - if ($el.titleLocation) { X "$indent$(Map-TitleLoc "$($el.titleLocation)")" } - } elseif ($smartDefault) { - X "$indent$smartDefault" - } -} - -function Emit-Group { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. - $groupVal = "$($el.group)".ToLower() - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "alwayshorizontal" { "AlwaysHorizontal" } - "alwaysvertical" { "AlwaysVertical" } - "horizontalifpossible" { "HorizontalIfPossible" } - "collapsible" { "Vertical" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - - # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). - # Legacy: group:'collapsible' эквивалентно behavior:'collapsible'. - $behaviorVal = if ($el.behavior) { "$($el.behavior)".ToLower() } elseif ($groupVal -eq "collapsible") { "collapsible" } else { $null } - $bmap = @{ "usual"="Usual"; "collapsible"="Collapsible"; "popup"="PopUp" } - if ($behaviorVal -and $bmap.ContainsKey($behaviorVal)) { - X "$inner$($bmap[$behaviorVal])" - } - # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) - if ($el.collapsed -eq $true) { X "$innertrue" } - - # Representation - if ($el.representation) { - $repr = switch ("$($el.representation)") { - "none" { "None" } - "normal" { "NormalSeparation" } - "weak" { "WeakSeparation" } - "strong" { "StrongSeparation" } - default { "$($el.representation)" } - } - X "$inner$repr" - } - - # ShowTitle - if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } - # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст - if ($el.collapsedTitle) { Emit-MLText -tag "CollapsedRepresentationTitle" -text $el.collapsedTitle -indent $inner } - - # United - if ($el.united -eq $false) { X "$innerfalse" } - - # Формат значения пути к данным заголовка (; парный к titleDataPath группы) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) — перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-ColumnGroup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - # Group orientation (horizontal / vertical / inCell — последнее только здесь) - $groupVal = "$($el.columnGroup)" - $orientation = switch ($groupVal) { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "inCell" { "InCell" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - - if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } - # showInHeader эмитится общим Emit-CommonElementProps (через Emit-Layout) - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) - Emit-ColumnPics -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) — перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Input { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { - $loc = switch ("$($el.titleLocation)") { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - default { "$($el.titleLocation)" } - } - X "$inner$loc" - } - - if ($null -ne $el.multiLine) { X "$inner$(if ($el.multiLine){'true'}else{'false'})" } - if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } - # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, - # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). - if ($null -ne $el.choiceButton) { X "$inner$(if ($el.choiceButton){'true'}else{'false'})" } - # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) - if ($null -ne $el.clearButton) { X "$inner$(if ($el.clearButton){'true'}else{'false'})" } - if ($null -ne $el.spinButton) { X "$inner$(if ($el.spinButton){'true'}else{'false'})" } - if ($null -ne $el.dropListButton) { X "$inner$(if ($el.dropListButton){'true'}else{'false'})" } - if ($null -ne $el.choiceListButton) { X "$inner$(if ($el.choiceListButton){'true'}else{'false'})" } - if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-ColumnPics -el $el -indent $inner - if ($el.textEdit -eq $false) { X "$innerfalse" } - # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) - foreach ($p in @( - @('wrap','Wrap'), @('openButton','OpenButton'), @('listChoiceMode','ListChoiceMode'), - @('extendedEditMultipleValues','ExtendedEditMultipleValues'), @('chooseType','ChooseType'), - @('quickChoice','QuickChoice'), @('autoChoiceIncomplete','AutoChoiceIncomplete') - )) { - if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } - } - # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. - # availableTypes — формат типа реквизита (§type); Emit-Type сам разбирает мультитип "a | b". - if ($null -ne $el.typeDomainEnabled) { X "$inner$(if ($el.typeDomainEnabled){'true'}else{'false'})" } - if ($el.availableTypes) { Emit-Type -typeStr $el.availableTypes -indent $inner -tag 'AvailableTypes' } - # InputField-специфичные value-скаляры - foreach ($p in @( - @('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'), - @('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath') - )) { - if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")" } - } - # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). - foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) { - if ($null -ne $el.($p[0])) { - $mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' } - X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")" - } - } - if ($el.choiceButtonRepresentation) { X "$inner$($el.choiceButtonRepresentation)" } - Emit-PictureRef -val $el.choiceButtonPicture -picTag 'ChoiceButtonPicture' -indent $inner - Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true)) - - if ($el.inputHint) { - Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner - } - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - Emit-ChoiceList -el $el -indent $inner - - # Связи по типу / связи параметров выбора / параметры выбора - Emit-TypeLink -el $el -indent $inner - Emit-ChoiceParameterLinks -el $el -indent $inner - Emit-ChoiceParameters -el $el -indent $inner - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" - - X "$indent" -} - -function Emit-Check { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-ColumnPics -el $el -indent $inner - # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг - if ($null -ne $el.PSObject.Properties['checkBoxType']) { - if ($el.checkBoxType) { - $cbt = switch ("$($el.checkBoxType)".ToLower()) { 'auto' {'Auto'} 'checkbox' {'CheckBox'} 'switcher' {'Switcher'} 'tumbler' {'Tumbler'} default {"$($el.checkBoxType)"} } - X "$inner$cbt" - } - } else { X "$innerAuto" } - - Emit-TitleLocation -el $el -indent $inner -smartDefault "Right" - - Emit-Layout -el $el -indent $inner - - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" - - X "$indent" -} - -# Maps Russian/English root of a typed reference path to canonical English root. -# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y". -$script:refRootSynonyms = @{ - "Перечисление" = "Enum" - "Справочник" = "Catalog" - "Документ" = "Document" - "ПланСчетов" = "ChartOfAccounts" - "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" - "ПланВидовРасчета" = "ChartOfCalculationTypes" - "ПланВидовРасчёта" = "ChartOfCalculationTypes" - "ПланОбмена" = "ExchangePlan" - "БизнесПроцесс" = "BusinessProcess" - "Задача" = "Task" - "РегистрСведений" = "InformationRegister" - "РегистрНакопления" = "AccumulationRegister" - "РегистрБухгалтерии" = "AccountingRegister" - "РегистрРасчета" = "CalculationRegister" - "РегистрРасчёта" = "CalculationRegister" - "ЖурналДокументов" = "DocumentJournal" - "КритерийОтбора" = "FilterCriterion" -} -$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления") - -# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты". -# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений. -function Normalize-MetaTypeRef { - param([string]$ref) - if ([string]::IsNullOrEmpty($ref)) { return $ref } - $dot = $ref.IndexOf('.') - if ($dot -lt 1) { return $ref } - $root = $ref.Substring(0, $dot) - if ($script:refRootSynonyms.ContainsKey($root)) { - return $script:refRootSynonyms[$root] + $ref.Substring($dot) - } - return $ref -} - -# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." } -function Normalize-ChoiceValue { - param($value) - - # Booleans - if ($value -is [bool]) { - return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } } - } - # Numbers (int / decimal / double) - if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) { - return @{ XsiType = "xs:decimal"; Text = "$value" } - } - - $s = "$value" - if ([string]::IsNullOrEmpty($s)) { - return @{ XsiType = "xs:string"; Text = "" } - } - - # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime - if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { - return @{ XsiType = "xs:dateTime"; Text = $s } - } - - # Try to detect typed reference path: ".[..]" - $parts = $s -split '\.' - if ($parts.Count -ge 2) { - $root = $parts[0] - $canonRoot = $null - if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] } - elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root } - - if ($canonRoot) { - $typeName = $parts[1] - $normalized = $null - - if ($canonRoot -eq "Enum") { - if ($parts.Count -eq 2) { - # "Enum.X" alone — not a value, treat as string - } elseif ($parts.Count -eq 3) { - # "Enum.X.Y" — insert .EnumValue. ("EmptyRef" — пустая ссылка, БЕЗ вставки) - if ($parts[2] -eq 'EmptyRef') { $normalized = "Enum.$typeName.EmptyRef" } - else { $normalized = "Enum.$typeName.EnumValue.$($parts[2])" } - } else { - # "Enum.X..Y..." — replace member with EnumValue (handles ЗначениеПеречисления too) - $member = $parts[2] - if ($script:enumValueSynonyms -contains $member) { - $rest = $parts[3..($parts.Count-1)] -join '.' - $normalized = "Enum.$typeName.EnumValue.$rest" - } else { - $rest = $parts[2..($parts.Count-1)] -join '.' - $normalized = "Enum.$typeName.EnumValue.$rest" - } - } - } else { - # Other ref roots: just translate root, keep tail as-is - if ($parts.Count -ge 3) { - $tail = $parts[1..($parts.Count-1)] -join '.' - $normalized = "$canonRoot.$tail" - } - } - - if ($normalized) { - return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized } - } - } - } - - return @{ XsiType = "xs:string"; Text = $s } -} - -# Emit Presentation block for a choiceList item. -# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}. -# Empty/null → emits empty . -function Emit-ChoicePresentation { - param($pres, [string]$indent) - if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) { - X "$indent" - return - } - - $pairs = @() - if ($pres -is [string]) { - $pairs += ,@("ru", $pres) - } elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) { - foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") } - } elseif ($pres.PSObject -and $pres.PSObject.Properties) { - foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") } - } else { - $pairs += ,@("ru", "$pres") - } - - X "$indent" - foreach ($pair in $pairs) { - X "$indent`t" - X "$indent`t`t$($pair[0])" - X "$indent`t`t$(Esc-Xml $pair[1])" - X "$indent`t" - } - X "$indent" -} - -# для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). -function Get-ChoiceValueTag { - param($norm) - if ([string]::IsNullOrEmpty($norm.Text)) { return "" } - return "$(Esc-Xml $norm.Text)" -} - -# Emit (список выбора) — у RadioButtonField и InputField. -# Элемент: { value, presentation?/title? } (+ рус. синонимы значение/представление). -function Emit-ChoiceList { - param($el, [string]$indent) - if (-not $el.choiceList -or $el.choiceList.Count -eq 0) { return } - X "$indent" - $itemIndent = "$indent`t" - foreach ($item in $el.choiceList) { - # value (+ рус. синоним "значение") - $valRaw = $null - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("value")) { $valRaw = $item["value"] } - elseif ($item.Contains("значение")) { $valRaw = $item["значение"] } - } else { - if ($item.PSObject.Properties["value"]) { $valRaw = $item.value } - elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" } - } - - # presentation (presentation OR title синоним) - $presRaw = $null - $hasPres = $false - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true } - elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true } - elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true } - } else { - if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true } - elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true } - elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true } - } - - # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — - # переопределяет авто-детект (Normalize-ChoiceValue вывела бы xs:string). - $vtRaw = $null - if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { - if ($item.Contains("valueType")) { $vtRaw = "$($item["valueType"])" } - } elseif ($item.PSObject.Properties["valueType"]) { $vtRaw = "$($item.valueType)" } - - if ($vtRaw) { $norm = @{ XsiType = $vtRaw; Text = "$valRaw" } } - else { $norm = Normalize-ChoiceValue -value $valRaw } - - # авто-вывод presentation, если не задан - if (-not $hasPres) { - if ($norm.XsiType -eq "xr:DesignTimeRef") { - $tail = ($norm.Text -split '\.')[-1] - $presRaw = Title-FromName -name $tail - } else { - $presRaw = $norm.Text - } - } - - X "$itemIndent" - $valIndent = "$itemIndent`t" - X "$valIndent" - X "$valIndent0" - X "$valIndent" - Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t" - X "$valIndent`t$(Get-ChoiceValueTag $norm)" - X "$valIndent" - X "$itemIndent" - } - X "$indent" -} - -# Чтение свойства из hashtable/PSCustomObject по списку синонимов (первый найденный, иначе $null). -function Get-ElProp { - param($obj, [string[]]$names) - if ($null -eq $obj) { return $null } - foreach ($n in $names) { - if ($obj -is [System.Collections.IDictionary]) { - if ($obj.Contains($n)) { return $obj[$n] } - } elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { - return $obj.PSObject.Properties[$n].Value - } - } - return $null -} - -# Приведение строкового литерала shorthand к типу: true/false → bool, целое/дробное → число, -# иначе строка (ref-путь нормализует уже Normalize-ChoiceValue). -function ConvertTo-ScalarLiteral { - param([string]$s) - $t = "$s".Trim() - if ($t -match '^(?i:true)$') { return $true } - if ($t -match '^(?i:false)$') { return $false } - if ($t -match '^-?\d+$') { return [int]$t } - if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) } - return $t -} - -# Shorthand параметра выбора: "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. -function ConvertFrom-ChoiceParamShorthand { - param([string]$s) - $eq = $s.IndexOf('=') - if ($eq -lt 0) { return @{ name = $s.Trim() } } - $name = $s.Substring(0, $eq).Trim() - $rest = $s.Substring($eq + 1) - if ($rest -match ',') { - $vals = @() - foreach ($part in ($rest -split ',')) { $vals += ,(ConvertTo-ScalarLiteral $part) } - return @{ name = $name; value = $vals } - } - return @{ name = $name; value = (ConvertTo-ScalarLiteral $rest) } -} - -# Shorthand связи параметров выбора: "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. -function ConvertFrom-ChoiceParamLinkShorthand { - param([string]$s) - $eq = $s.IndexOf('=') - if ($eq -lt 0) { return @{ name = $s.Trim() } } - $o = @{ name = $s.Substring(0, $eq).Trim() } - $rest = $s.Substring($eq + 1).Trim() - if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { - $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] - } else { - $o['dataPath'] = $rest - } - return $o -} - -# Shorthand связи по типу: "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. -function ConvertFrom-TypeLinkShorthand { - param([string]$s) - if ($s -match '^(.*)#(\d+)$') { return @{ dataPath = $matches[1].Trim(); linkItem = [int]$matches[2] } } - return @{ dataPath = "$s".Trim() } -} - -# Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . -# Скаляр → один Value (через Normalize-ChoiceValue); массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. -function Emit-ChoiceParamValue { - # $isArray передаётся ЯВНО из вызывающего кода: PowerShell разворачивает одноэлементный массив - # при биндинге параметра ($value становится скаляром), поэтому определять массив тут — ненадёжно - # (1-элементный список `["X"]` эмитился бы скаляром вместо FixedArray). foreach по скаляру = 1 итерация. - param($value, [string]$indent, [bool]$isArray) - X "$indent" - if ($isArray) { - X "$indent" - foreach ($v in $value) { - $norm = Normalize-ChoiceValue -value $v - X "$indent`t" - X "$indent`t`t" - X "$indent`t`t$(Get-ChoiceValueTag $norm)" - X "$indent`t" - } - X "$indent" - } else { - $norm = Normalize-ChoiceValue -value $value - X "$indent$(Get-ChoiceValueTag $norm)" - } -} - -# (параметры выбора поля ввода) — [{name, value}]. value через Normalize-ChoiceValue; -# массив значений → FixedArray. Рус. синонимы имя/значение. -function Emit-ChoiceParameters { - param($el, [string]$indent) - $cp = $el.choiceParameters - if (-not $cp -or @($cp).Count -eq 0) { return } - X "$indent" - foreach ($item in @($cp)) { - if ($item -is [string]) { $item = ConvertFrom-ChoiceParamShorthand $item } - $name = Get-ElProp $item @('name','имя') - # Наличие ключа value (≠ значения) + ПРЯМОЙ доступ к значению (без Get-ElProp): его return - # разворачивает 1-элементный массив (PS unwrap), теряя массив-ность → FixedArray не эмитится. - # Индексер/member-доступ массив сохраняет; if-выражение/функция-return — нет. - $hasVal = $false; $val = $null - if ($item -is [System.Collections.IDictionary]) { - if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] } - elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] } - } else { - if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value } - elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value } - } - $valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string]) - X "$indent`t" - # Параметр выбора без значения → (платформа, 13 в корпусе); - # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. - if (-not $hasVal) { - X "$indent`t`t" - } else { - X "$indent`t`t" - Emit-ChoiceParamValue -value $val -indent "$indent`t`t`t" -isArray $valIsArray - X "$indent`t`t" - } - X "$indent`t" - } - X "$indent" -} - -# (связи параметров выбора) — [{name, dataPath, valueChange?}]. -# valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. -function Emit-ChoiceParameterLinks { - param($el, [string]$indent) - $cpl = $el.choiceParameterLinks - if (-not $cpl -or @($cpl).Count -eq 0) { return } - X "$indent" - foreach ($lk in @($cpl)) { - if ($lk -is [string]) { $lk = ConvertFrom-ChoiceParamLinkShorthand $lk } - $name = Get-ElProp $lk @('name','имя') - $dp = Get-ElProp $lk @('dataPath','path','путь') - $vcRaw = Get-ElProp $lk @('valueChange','режимИзменения') - $vc = "Clear" - if ($vcRaw) { - $vc = switch -Regex ("$vcRaw".ToLower()) { - '^(clear|очистить|очистка)$' { "Clear"; break } - '^(dontchange|неизменять|неменять|нет)$' { "DontChange"; break } - default { "$vcRaw" } - } - } - X "$indent`t" - X "$indent`t`t$(Esc-Xml "$name")" - X "$indent`t`t$(Esc-Xml "$dp")" - X "$indent`t`t$vc" - X "$indent`t" - } - X "$indent" -} - -# (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. -function Emit-TypeLink { - param($el, [string]$indent) - $tl = $el.typeLink - if (-not $tl) { return } - if ($tl -is [string]) { $tl = ConvertFrom-TypeLinkShorthand $tl } - $dp = Get-ElProp $tl @('dataPath','path','путь') - $li = Get-ElProp $tl @('linkItem','элементСвязи') - if ($null -eq $li) { $li = 0 } - X "$indent" - X "$indent`t$(Esc-Xml "$dp")" - X "$indent`t$li" - X "$indent" -} - -function Emit-Radio { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-TitleLocation -el $el -indent $inner -smartDefault "None" - - # RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms. - $rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" } - $rbt = switch -Regex ($rbtRaw.ToLower()) { - '^(auto|авто)$' { "Auto"; break } - '^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break } - '^(tumbler|тумблер)$' { "Tumbler"; break } - default { $rbtRaw } - } - X "$inner$rbt" - - if ($null -ne $el.columnsCount) { - X "$inner$($el.columnsCount)" - } - - Emit-ChoiceList -el $el -indent $inner - - Emit-Layout -el $el -indent $inner - - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio" - - X "$indent" -} - -# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму -# (reuse Resolve-MLFormatted, как у extendedTooltip). Атрибут formatted эмитится ВСЕГДА -# (специфика декораций). Sibling-ключ `formatted` — back-compat override авто-детекта. -function Emit-DecorationTitle { - param($el, [string]$name, [string]$indent, [switch]$auto) - $hasKey = $null -ne $el.PSObject.Properties['title'] - $titleVal = if ($hasKey) { $el.title } elseif ($auto -and $name) { Title-FromName -name $name } else { $null } - if ($titleVal) { - $r = Resolve-MLFormatted $titleVal - $fmt = if ($null -ne $el.PSObject.Properties['formatted']) { [bool]$el.formatted } else { $r.formatted } - X "$indent<Title formatted=`"$(if ($fmt) { 'true' } else { 'false' })`">" - Emit-MLItems -val $r.text -indent "$indent`t" - X "$indent" - } - if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } - if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } -} - -function Emit-Label { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title - # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). - Emit-CommonFlags -el $el -indent $inner - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $el -indent $inner - Emit-Appearance -el $el -indent $inner -profile 'decoration' - - Emit-DecorationTitle -el $el -name $name -indent $inner -auto - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" - - X "$indent" -} - -function Emit-LabelField { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - if ($el.editMode) { X "$inner$($el.editMode)" } - # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode - if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } - # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение - if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } - Emit-ColumnPics -el $el -indent $inner - # ВНИМАНИЕ: у LabelField платформенный тег именно (опечатка 1С), не . - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $el -indent $inner - - if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } - if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - - # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" - - X "$indent" -} - -# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). -# Платформа всегда эмитит этот блок на дин-список-таблице; компилятор зеркалит дефолты, -# DSL-ключ переопределяет; декомпилятор инвертирует (опускает значения = дефолту). -function Emit-DynListTableBlock { - param($el, [string]$indent) - # (useAlternationRowColor — общее свойство таблицы, эмитится в Emit-Table) - # Group A (гарант. блок, n=5079): дефолт + override - $ar = if ($el.autoRefresh -eq $true) { "true" } else { "false" } - X "$indent$ar" - $arp = if ($el.PSObject.Properties["autoRefreshPeriod"] -and $null -ne $el.autoRefreshPeriod) { $el.autoRefreshPeriod } else { 60 } - X "$indent$arp" - X "$indent" - X "$indent`tCustom" - X "$indent`t0001-01-01T00:00:00" - X "$indent`t0001-01-01T00:00:00" - X "$indent" - $cfi = if ($el.choiceFoldersAndItems) { $el.choiceFoldersAndItems } else { "Items" } - X "$indent$cfi" - $rcr = if ($el.restoreCurrentRow -eq $true) { "true" } else { "false" } - X "$indent$rcr" - X "$indent" - $sr = if ($el.showRoot -eq $false) { "false" } else { "true" } - X "$indent$sr" - $arc = if ($el.allowRootChoice -eq $true) { "true" } else { "false" } - X "$indent$arc" - $uodc = if ($el.updateOnDataChange) { $el.updateOnDataChange } else { "Auto" } - X "$indent$uodc" - if ($el.userSettingsGroup) { X "$indent$($el.userSettingsGroup)" } - $agcru = if ($el.allowGettingCurrentRowURL -eq $false) { "false" } else { "true" } - X "$indent$agcru" -} - -function Emit-Table { - param($el, [string]$name, [int]$id, [string]$indent) - - $script:currentTableName = $name # дефолт source для кастомных дополнений в commandBar - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.representation) { - X "$inner$($el.representation)" - } - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - # ChangeRowSet/Order — эмитим явное значение (в т.ч. false: платформа пишет его на ValueTable) - if ($el.PSObject.Properties['changeRowSet'] -and $null -ne $el.changeRowSet) { - X "$inner$(if ($el.changeRowSet -eq $true){'true'}else{'false'})" - } - if ($el.PSObject.Properties['changeRowOrder'] -and $null -ne $el.changeRowOrder) { - X "$inner$(if ($el.changeRowOrder -eq $true){'true'}else{'false'})" - } - if ($el.autoInsertNewRow -eq $true) { X "$innertrue" } - # RowFilter — nil-плейсхолдер (всегда пустой); ключ присутствует → эмитим - if ($el.PSObject.Properties['rowFilter']) { X "$inner" } - # Высота в строках таблицы () — отдельное свойство от (высота элемента, - # эмитится generic-ом Emit-Layout ниже). Таблица может нести оба (237 в корпусе). - if ($el.heightInTableRows) { X "$inner$($el.heightInTableRows)" } - if ($el.header -eq $false) { X "$inner
false
" } - if ($el.footer -eq $true) { X "$inner
true
" } - - if ($el.commandBarLocation) { - X "$inner$($el.commandBarLocation)" - } - if ($el.searchStringLocation) { - X "$inner$($el.searchStringLocation)" - } - if ($el.choiceMode -eq $true) { X "$innertrue" } - # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). - if ($null -ne $el.autofill) { X "$inner$(if ($el.autofill){'true'}else{'false'})" } - if ($el.multipleChoice -eq $true) { X "$innertrue" } - if ($el.searchOnInput) { X "$inner$($el.searchOnInput)" } - if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } - # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) - if ($null -ne $el.headerHeight) { X "$inner$($el.headerHeight)" } - if ($null -ne $el.footerHeight) { X "$inner$($el.footerHeight)" } - if ($el.useAlternationRowColor -eq $true) { X "$innertrue" } - if ($el.selectionMode) { X "$inner$($el.selectionMode)" } - if ($el.rowSelectionMode) { X "$inner$($el.rowSelectionMode)" } - if ($el.verticalLines -eq $false) { X "$innerfalse" } - if ($el.horizontalLines -eq $false) { X "$innerfalse" } - if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } - if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } - if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } - # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) - Emit-PictureRef -val $el.rowsPicture -picTag 'RowsPicture' -indent $inner - # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) - if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } - # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) - if ($el.refreshRequest) { X "$inner$($el.refreshRequest)" } - # Блок свойств дин-список-таблицы (помечена эвристикой 11b.4) - if ($el.PSObject.Properties["_dynList"] -and $el._dynList) { Emit-DynListTableBlock -el $el -indent $inner } - if ($el.viewStatusLocation) { X "$inner$($el.viewStatusLocation)" } - if ($el.searchControlLocation) { X "$inner$($el.searchControlLocation)" } - Emit-Layout -el $el -indent $inner - - # CommandSet таблицы эмитится через Emit-Layout (общий механизм поля) - - # Оформление (цвета/граница таблицы) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - # AutoCommandBar: приоритет commandBar-свойства (контент); иначе tableAutofill-shorthand; иначе пусто. - if ($null -ne $el.commandBar) { - Emit-CompanionPanel -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner -panel $el.commandBar - } elseif ($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 "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - $adds = $el.additions - Emit-TableAddition -typeKey 'searchString' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchString') - Emit-TableAddition -typeKey 'viewStatus' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'viewStatus') - Emit-TableAddition -typeKey 'searchControl' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchControl') - - # Columns - if ($el.columns -and $el.columns.Count -gt 0) { - X "$inner" - foreach ($col in $el.columns) { - Emit-Element -el $col -indent "$inner`t" - } - X "$inner" - } - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" - - X "$indent
" -} - -function Emit-Pages { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.pagesRepresentation) { - X "$inner$($el.pagesRepresentation)" - } - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Companion - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" - - # Children (pages) - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Page { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner -auto - Emit-CommonFlags -el $el -indent $inner - - # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). - # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. - Emit-PictureRef -val $el.picture -picTag 'Picture' -indent $inner - - if ($el.group) { - # Доступные значения страницы/обычной группы: Vertical / HorizontalIfPossible / AlwaysHorizontal - # (InCell — только у columnGroup). Horizontal/AlwaysVertical оставлены forgiving (legacy). - $orientation = switch ("$($el.group)") { - "horizontal" { "Horizontal" } - "vertical" { "Vertical" } - "alwaysHorizontal" { "AlwaysHorizontal" } - "alwaysVertical" { "AlwaysVertical" } - "horizontalIfPossible" { "HorizontalIfPossible" } - default { $null } - } - if ($orientation) { X "$inner$orientation" } - } - if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } - # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) - if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } - if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } - Emit-Layout -el $el -indent $inner - - # Оформление страницы (BackColor / TitleTextColor / TitleFont) — после ShowTitle, перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companion - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" - } - X "$inner" - } - - X "$indent" -} - -function Emit-Button { - param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false) - - X "$indent" -} - -function Emit-PictureDecoration { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-DecorationTitle -el $el -name $name -indent $inner - # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) - if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } - Emit-CommonFlags -el $el -indent $inner - - # Источник картинки — ТОЛЬКО $el.src (у PictureDecoration ключ 'picture' = тип/имя элемента, не источник). - # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . - if ($el.src) { - $srcStr = "$($el.src)" - $lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" } - X "$inner" - if ($srcStr -match '^abs:(.*)$') { X "$inner`t$(Esc-Xml $matches[1])" } - else { X "$inner`t$(Esc-Xml $srcStr)" } - X "$inner`t$lt" - if ($el.transparentPixel) { X "$inner`t" } - X "$inner" - } - - if ($el.hyperlink -eq $true) { X "$innertrue" } - Emit-Layout -el $el -indent $inner - - # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) - Emit-Appearance -el $el -indent $inner -profile 'decoration' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" - - X "$indent" -} - -function Emit-PictureField { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner - Emit-CommonFlags -el $el -indent $inner - - if ($el.editMode) { X "$inner$($el.editMode)" } - Emit-ColumnPics -el $el -indent $inner - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - if ($el.hyperlink -eq $true) { X "$innertrue" } - - Emit-Layout -el $el -indent $inner - - # ValuesPicture — picture (collection) used to render the field's value. - # Required for a Boolean-bound PictureField to actually show an icon. - # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. - Emit-PictureRef -val $el.valuesPicture -picTag 'ValuesPicture' -indent $inner - if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" - - X "$indent" -} - -function Emit-Calendar { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - - if ($el.titleLocation) { - $loc = switch ("$($el.titleLocation)") { - "none" { "None" } - "left" { "Left" } - "right" { "Right" } - "top" { "Top" } - "bottom" { "Bottom" } - "auto" { "Auto" } - default { "$($el.titleLocation)" } - } - X "$inner$loc" - } - - Emit-Layout -el $el -indent $inner - - # Календарно-специфичные свойства (порядок схемы: после layout, до companions) - if ($el.selectionMode) { X "$inner$($el.selectionMode)" } - if ($null -ne $el.showCurrentDate) { $v = if ($el.showCurrentDate) { "true" } else { "false" }; X "$inner$v" } - if ($null -ne $el.widthInMonths) { X "$inner$($el.widthInMonths)" } - if ($null -ne $el.heightInMonths) { X "$inner$($el.heightInMonths)" } - if ($null -ne $el.showMonthsPanel) { $v = if ($el.showMonthsPanel) { "true" } else { "false" }; X "$inner$v" } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" - - X "$indent" -} - -# Спец-поля «документ/датчик» (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): -# единый скелет поля (path/title/flags/titleLocation/editMode/layout/companions/events). -# Типоспец. enum/bool скаляры — через generic (Emit-Layout→Emit-GenericScalars); -# числовые скаляры датчиков (min/max/шаги) — без xsi:type (≠ типизированных InputField). -# enableDrag/enableStartDrag — общие (Emit-CommonElementProps), фактическое значение. -function Emit-SimpleField { - param($el, [string]$name, [int]$id, [string]$indent, [string]$xmlTag, [string]$typeKey) - - X "$indent<$xmlTag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" - $inner = "$indent`t" - - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - if ($el.editMode) { X "$inner$($el.editMode)" } - - Emit-Layout -el $el -indent $inner - - # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через Emit-Layout. - if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } - - # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) - foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'), @('largeStep','LargeStep'), @('markingStep','MarkingStep'), @('step','Step'))) { - if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$($el.($p[0]))" } - } - - # Оформление (цвета/шрифты/граница) — перед компаньонами - Emit-Appearance -el $el -indent $inner -profile 'field' - - # Companions - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - Emit-Events -el $el -elementName $name -indent $inner -typeKey $typeKey - - X "$indent" -} - -# GanttChartField — скелет поля + вложенная (полноценная таблица, через Emit-Element). -# Порядок (по корпусу): path/title/flags/titleLocation/layout/appearance, companions, Table, events. -function Emit-GanttChart { - param($el, [string]$name, [int]$id, [string]$indent) - X "$indent" - $inner = "$indent`t" - if ($el.path) { X "$inner$($el.path)" } - Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) - Emit-CommonFlags -el $el -indent $inner - if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } - Emit-Layout -el $el -indent $inner - Emit-Appearance -el $el -indent $inner -profile 'field' - Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем Emit-Element) - if ($el.ganttTable) { Emit-Element -el $el.ganttTable -indent $inner } - Emit-Events -el $el -elementName $name -indent $inner -typeKey "ganttChart" - X "$indent" -} - -function Emit-CommandBar { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.commandSource) { X "$inner$($el.commandSource)" } - - if ($el.autofill -eq $true) { X "$innertrue" } - - $hl = Get-HLocation $el; if ($hl) { X "$inner$hl" } - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -function Emit-ButtonGroup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner - - if ($el.commandSource) { X "$inner$($el.commandSource)" } - - if ($el.representation) { - X "$inner$($el.representation)" - } - - Emit-CommonFlags -el $el -indent $inner - Emit-Layout -el $el -indent $inner - - # Companion: ExtendedTooltip - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children (кнопки в контексте командной панели) - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -function Emit-Popup { - param($el, [string]$name, [int]$id, [string]$indent) - - X "$indent" - $inner = "$indent`t" - - Emit-Title -el $el -name $name -indent $inner -auto - Emit-CommonFlags -el $el -indent $inner - - Emit-CommandPicture -pic $el.picture -elemLt $el.loadTransparent -indent $inner - - if ($el.representation) { - X "$inner$($el.representation)" - } - Emit-Layout -el $el -indent $inner - - # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном - Emit-Appearance -el $el -indent $inner -profile 'field' - - Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip - - # Children - if ($el.children -and $el.children.Count -gt 0) { - X "$inner" - foreach ($child in $el.children) { - Emit-Element -el $child -indent "$inner`t" -inCmdBar $true - } - X "$inner" - } - - X "$indent" -} - -# --- 8. Attribute emitter --- - -# FunctionalOption.X — у Attribute/Command/Column. -# DSL: массив строк. Forgiving: "X" / "FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. -function Emit-FunctionalOptions { - param($fo, [string]$indent) - if (-not $fo -or @($fo).Count -eq 0) { return } - X "$indent" - foreach ($opt in @($fo)) { - $v = "$opt" - if ($v -match '^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$') { } # GUID — как есть - elseif ($v -match '^FunctionalOption\.') { } # уже с префиксом - else { $v = "FunctionalOption.$v" } - X "$indent`t$v" - } - X "$indent" -} - -# Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. -function Emit-AttrColumn { - param($col, [string]$indent) - $colId = New-Id - X "$indent" - if ($col.title) { Emit-MLText -tag "Title" -text $col.title -indent "$indent`t" } - Emit-Type -typeStr "$($col.type)" -indent "$indent`t" - Emit-FunctionalOptions -fo $col.functionalOptions -indent "$indent`t" - # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита - if ($null -ne $col.view) { Emit-XrFlag -tag 'View' -val $col.view -indent "$indent`t" } - if ($null -ne $col.edit) { Emit-XrFlag -tag 'Edit' -val $col.edit -indent "$indent`t" } - X "$indent" -} - -# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- -# Та же сущность, что параметры СКД (см. skd-compile), но в форме: обёртка -# + дети с префиксом dcssch:. DSL переиспользует грамматику параметров СКД (shorthand + -# объект). Контекстные дефолты дин-списка (паттерн «умный дефолт у всегда-эмитируемого тега»): -# useRestriction — эмитим ВСЕГДА, дефолт TRUE (в СКД дефолт false); -# title — авто из имени (Title-FromName), если ключ не задан. -# Канон. порядок детей (по корпусу acc/erp 8.3.24): name, title, valueType, value, -# useRestriction, expression, availableValue*, valueListAllowed, availableAsField, -# inputParameters, denyIncompleteValues, use. - -# Многоязычный текст в DCS-контексте — с xsi:type="v8:LocalStringType" (form-compile -# Emit-MLText его НЕ ставит, т.к. формовые голые; в dcssch:* — обязателен). -function Emit-DLMLText { - param([string]$tag, $text, [string]$indent) - X "$indent<$tag xsi:type=`"v8:LocalStringType`">" - Emit-MLItems -val $text -indent "$indent`t" - X "$indent</$tag>" -} - -function Has-DLProp { - param($obj, [string]$name) - if ($null -eq $obj) { return $false } - if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } - if ($obj.PSObject -and $obj.PSObject.Properties[$name]) { return $true } - return $false -} - -function Split-DLValueListCsv { - param([string]$s) - $result = @() - if ($null -eq $s) { return ,$result } - $items = @(); $buf = New-Object System.Text.StringBuilder; $inQuote = $null - for ($i = 0; $i -lt $s.Length; $i++) { - $ch = $s[$i] - if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } - elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } - elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } - else { [void]$buf.Append($ch) } - } - if ($buf.Length -gt 0) { $items += $buf.ToString() } - foreach ($raw in $items) { - $t = $raw.Trim() - if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } - if ($t -ne "") { $result += $t } - } - return ,$result -} - -# Shorthand: "Имя [Заголовок]: Тип = Значение @valueList @hidden" -function Parse-DLParamShorthand { - param([string]$s) - $result = @{ name = ""; type = ""; value = $null; title = $null } - if ($s -match '@valueList') { $result.valueListAllowed = $true; $s = $s -replace '\s*@valueList', '' } - if ($s -match '@hidden') { $result.hidden = $true; $s = $s -replace '\s*@hidden', '' } - if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim(); $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } - # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). - if ($s -match '^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$') { - $result.name = $Matches[1].Trim() - $typeRaw = $Matches[2].Trim() - if ($typeRaw -match '[|+]') { - $result.type = (($typeRaw -split '\s*[|+]\s*') | ForEach-Object { Resolve-TypeStr ($_.Trim()) }) -join ' | ' - } else { - $result.type = Resolve-TypeStr $typeRaw - } - if ($Matches[4]) { - $rhs = $Matches[4].Trim() - $items = Split-DLValueListCsv $rhs - if ($items.Count -ge 2) { $result.value = $items; $result.valueListAllowed = $true } - elseif ($items.Count -eq 1) { $result.value = $items[0] } - else { $result.value = $rhs } - } - } else { $result.name = $s.Trim() } - return $result -} - -function Test-DLEmptyValue { - param($v) - if ($null -eq $v) { return $true } - $s = "$v".Trim() - if ($s -eq "" -or $s -eq "_" -or $s.ToLowerInvariant() -eq "null") { return $true } - return $false -} - -# Эмиссия <dcssch:value> по типу/значению (xs:* или dcscor:DesignTimeValue для ссылок). -function Emit-DLValue { - param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) - if (Test-DLEmptyValue $val) { - # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil, даже при известном - # типе (в отличие от типизированного пустого в параметрах отчёта СКД). - if ($valueListAllowed) { return } - X "$indent<dcssch:value xsi:nil=`"true`"/>" - return - } - $valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" } - if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } - else { - if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } - elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } - else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } - } -} - -# <dcssch:valueType> — обёртка + тело через Emit-SingleType (ref-типы → cfg:, как в форме). -function Emit-DLValueType { - param($typeStr, [string]$indent) - if (-not $typeStr) { return } - X "$indent<dcssch:valueType>" - $parts = "$typeStr" -split '\s*[|+]\s*' - foreach ($part in $parts) { Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" } - X "$indent</dcssch:valueType>" -} - -function Emit-DLAvailableValue { - param($av, [string]$type, [string]$indent) - X "$indent<dcssch:availableValue>" - $avVal = if (Has-DLProp $av 'value') { $av.value } else { $null } - Emit-DLValue -type $type -val $avVal -indent "$indent`t" -valueListAllowed $false - $pres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { $null } - if ($pres) { Emit-DLMLText -tag "dcssch:presentation" -text $pres -indent "$indent`t" } - X "$indent</dcssch:availableValue>" -} - -# <dcssch:inputParameters> — ChoiceParameters / ChoiceParameterLinks / простое значение (порт из skd). -function Emit-DLInputParameters { - param($ip, [string]$indent) - if ($null -eq $ip) { return } - $items = @($ip) - if ($items.Count -eq 0) { return } - X "$indent<dcssch:inputParameters>" - foreach ($item in $items) { - X "$indent`t<dcscor:item>" - if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } - X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" - if (Has-DLProp $item 'choiceParameters') { - $cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() } - if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" } - else { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" - foreach ($cpItem in $cpItems) { - X "$indent`t`t`t<dcscor:item>" - X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" - foreach ($v in @($cpItem.values)) { - if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" } - elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" } - else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" } - } - X "$indent`t`t`t</dcscor:item>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif (Has-DLProp $item 'choiceParameterLinks') { - $cplItems = if ($null -ne $item.choiceParameterLinks) { @($item.choiceParameterLinks) } else { @() } - if ($cplItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" } - else { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" - foreach ($cplItem in $cplItems) { - X "$indent`t`t`t<dcscor:item>" - X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" - X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" - $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } - X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" - X "$indent`t`t`t</dcscor:item>" - } - X "$indent`t`t</dcscor:value>" - } - } elseif (Has-DLProp $item 'value') { - $val = $item.value - if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" } - elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" } - elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } - else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" } - } - X "$indent`t</dcscor:item>" - } - X "$indent</dcssch:inputParameters>" -} - -# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd-compile ── -# Грамматика идентична СКД: shorthand "Имя = Значение @off @user" или объект -# {parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation?}. -function Test-EmptyValue { - param($v) - if ($null -eq $v) { return $true } - $s = "$v".Trim() - if ($s -eq "") { return $true } - if ($s -eq "_") { return $true } - if ($s.ToLowerInvariant() -eq "null") { return $true } - return $false -} -function Emit-EmptyValue { - param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) - if ($valueListAllowed) { return } - $t = if ($null -eq $type) { "" } else { "$type" } - $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } - $pf = $tagPrefix - if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" } - elseif ($t -eq "StandardPeriod") { - X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" - X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" - X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" - X "$indent</${pf}value>" - } - elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" } - elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" } - elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" } - elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" } - else { X "$indent<${pf}value xsi:nil=`"true`"/>" } -} -function Parse-DataParamShorthand { - param([string]$s) - $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } - if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } - if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } - if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } - if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } - $s = $s.Trim() - if ($s -match '^([^=]+)=\s*(.+)$') { - $result.parameter = $Matches[1].Trim() - $valStr = $Matches[2].Trim() - $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") - if ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } - elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valStr } - elseif ($valStr -eq "true" -or $valStr -eq "false") { $result.value = [bool]($valStr -eq "true") } - else { $result.value = $valStr } - } else { $result.parameter = $s } - return $result -} -function Emit-DataParameters { - param($items, [string]$indent, $blockViewMode = $null) - if (-not $items -or @($items).Count -eq 0) { return } - X "$indent<dcsset:dataParameters>" - foreach ($dp in @($items)) { - if ($dp -is [string]) { - $parsed = Parse-DataParamShorthand $dp - $dpObj = New-Object PSObject - $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter - if ($null -ne $parsed.value) { $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value } - if ($parsed.use -eq $false) { $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false } - if ($parsed.userSettingID) { $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID } - if ($parsed.viewMode) { $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode } - $dp = $dpObj - } - X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" - if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } - X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" - if ($dp.nilValue -eq $true) { - X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" - } elseif ((Test-EmptyValue $dp.value) -and $dp.valueType) { - # Явный типизированный пустой (xs:string-плейсхолдер и т.п.) - Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false - } elseif (Test-EmptyValue $dp.value) { - # Нет значения и нет valueType → НЕ эмитим value-узел (form дин-список: use=false плейсхолдер). - # (В отличие от skd-settings, где значение всегда присутствует.) - } elseif ($null -ne $dp.value) { - $vtype = "$($dp.valueType)" - if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable] -or $dp.value -is [System.Collections.IDictionary]) -and ($dp.value.variant)) { - $_hasDate = $false; $_hasSD = $false - if ($dp.value -is [PSCustomObject]) { $_hasDate = [bool]$dp.value.PSObject.Properties['date']; $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] } - else { $_hasDate = $dp.value.Contains('date'); $_hasSD = $dp.value.Contains('startDate') } - $_variantStr = "$($dp.value.variant)" - $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') - if ($_isSBD) { - $_d = $null - if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } - elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } - X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" - X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" - if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" } - X "$indent`t`t</dcscor:value>" - } else { - $_sd = $null; $_ed = $null - if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } } - else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } } - X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" - X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" - if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" } - X "$indent`t`t</dcscor:value>" - } - } elseif ($vtype -match '^[a-zA-Z]+:') { - $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } - X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" - } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { - X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>" - } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ($vtype -match '^decimal') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ($vtype -match '^string') { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { - X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } else { - X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" - } - } - if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" } - if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" } - if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } - X "$indent`t</dcscor:item>" - } - if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } - X "$indent</dcsset:dataParameters>" -} - -function Emit-DLParameter { - param($p, $parsed, [string]$indent) - X "$indent<Parameter>" - $ci = "$indent`t" - X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>" - # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. - $title = $null - if ($parsed.title) { $title = $parsed.title } - elseif ($p -isnot [string] -and (Has-DLProp $p 'title') -and $p.title) { $title = $p.title } - elseif ($p -isnot [string] -and (Has-DLProp $p 'presentation') -and $p.presentation) { $title = $p.presentation } - if ($null -eq $title -or ($title -is [string] -and $title -eq '')) { $title = Title-FromName -name $parsed.name } - Emit-DLMLText -tag "dcssch:title" -text $title -indent $ci - # valueType - if ($parsed.type) { Emit-DLValueType -typeStr $parsed.type -indent $ci } - # value (дефолт nil; при valueListAllowed пустое — опускаем) - $vla = [bool]$parsed.valueListAllowed - $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) - if ($valIsArray) { - foreach ($v in @($parsed.value)) { Emit-DLValue -type $parsed.type -val $v -indent $ci -valueListAllowed $false } - } elseif ($vla -and (Test-DLEmptyValue $parsed.value) -and $parsed.valueExplicit) { - # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа здесь пишет nil - X "$ci<dcssch:value xsi:nil=`"true`"/>" - } else { - Emit-DLValue -type $parsed.type -val $parsed.value -indent $ci -valueListAllowed $vla - } - # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. - $ur = $true - if ($p -isnot [string] -and (Has-DLProp $p 'useRestriction')) { $ur = [bool]$p.useRestriction } - X "$ci<dcssch:useRestriction>$(if ($ur) { 'true' } else { 'false' })</dcssch:useRestriction>" - # expression - $expr = $null - if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" } - if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" } - # availableValues - if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) { - foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci } - } - # valueListAllowed - if ($vla) { X "$ci<dcssch:valueListAllowed>true</dcssch:valueListAllowed>" } - # availableAsField=false (hidden или явный) - $aaf = $null - if ($parsed.hidden -eq $true) { $aaf = $false } - if ($p -isnot [string] -and (Has-DLProp $p 'availableAsField')) { $aaf = [bool]$p.availableAsField } - if ($aaf -eq $false) { X "$ci<dcssch:availableAsField>false</dcssch:availableAsField>" } - # inputParameters - if ($p -isnot [string] -and (Has-DLProp $p 'inputParameters') -and $p.inputParameters) { Emit-DLInputParameters -ip $p.inputParameters -indent $ci } - # denyIncompleteValues - if ($p -isnot [string] -and (Has-DLProp $p 'denyIncompleteValues') -and $p.denyIncompleteValues -eq $true) { X "$ci<dcssch:denyIncompleteValues>true</dcssch:denyIncompleteValues>" } - # use - $useVal = $null - if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" } - if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" } - X "$indent</Parameter>" -} - -function Emit-DLParameters { - param($params, [string]$indent) - if (-not $params) { return } - foreach ($p in @($params)) { - if ($p -is [string]) { - $parsed = Parse-DLParamShorthand $p - } else { - $resolvedType = "" - if ((Has-DLProp $p 'type') -and $p.type) { - if ($p.type -is [array] -or ($p.type -is [System.Collections.IList] -and $p.type -isnot [string])) { - $resolvedType = (@($p.type | ForEach-Object { Resolve-TypeStr "$_" })) -join ' | ' - } else { $resolvedType = Resolve-TypeStr "$($p.type)" } - } elseif ((Has-DLProp $p 'valueType') -and $p.valueType) { - $resolvedType = Resolve-TypeStr "$($p.valueType)" - } - $parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); valueExplicit = (Has-DLProp $p 'value'); title = $null } - if ((Has-DLProp $p 'valueListAllowed') -and $p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } - if ((Has-DLProp $p 'hidden') -and $p.hidden -eq $true) { $parsed.hidden = $true } - } - Emit-DLParameter -p $p -parsed $parsed -indent $indent - } -} - -function Emit-Attributes { - param($attrs, [string]$indent, $conditionalAppearance = $null) - - $hasCA = $conditionalAppearance -and @($conditionalAppearance).Count -gt 0 - # Платформа ВСЕГДА эмитит <Attributes> (100% корпуса; 162 формы — пустой <Attributes/>). - if ((-not $attrs -or $attrs.Count -eq 0) -and -not $hasCA) { X "$indent<Attributes/>"; return } - if (-not $attrs -or $attrs.Count -eq 0) { - # Нет реквизитов, но есть условное оформление (последний child <Attributes>) - X "$indent<Attributes>" - Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' - X "$indent</Attributes>" - return - } - - X "$indent<Attributes>" - $seenAttrs = @{} - foreach ($attr in $attrs) { - $attrId = New-Id - $attrName = "$($attr.name)" - Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute' - - X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">" - $inner = "$indent`t`t" - - # Title атрибута (зеркало Emit-Title): нет ключа → авто-вывод из имени (кроме main); - # title "" → подавить; непустой → эмитить как есть. - $hasTitleKey = $null -ne $attr.PSObject.Properties['title'] - if ($hasTitleKey) { - if ($attr.title) { Emit-MLText -tag "Title" -text $attr.title -indent $inner } - } elseif ($attr.main -ne $true) { - Emit-MLText -tag "Title" -text (Title-FromName -name $attrName) -indent $inner - } - - # Type - if ($attr.type) { - Emit-Type -typeStr "$($attr.type)" -indent $inner - } else { - X "$inner<Type/>" - } - # valueType: уточнение типа значений ValueList → <Settings xsi:type="v8:TypeDescription"> - # (та же грамматика типа, что и Type, включая составной "A | B"). Forgiving-синонимы. - # Три состояния: нет ключа → нет Settings; "" → пустой <Settings…/>; тип → с типом. - $vtSpec = $null; $hasVt = $false - foreach ($k in @('valueType','typeDescription','описаниеТипов','типЗначений')) { - if ($attr.PSObject.Properties[$k]) { $vtSpec = $attr.$k; $hasVt = $true; break } - } - if ($hasVt) { - Emit-Type -typeStr "$vtSpec" -indent $inner -tag "Settings" -tagAttrs ' xsi:type="v8:TypeDescription"' - } - # Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика). - # Идёт сразу после <Type> (как valueType/DynamicList Settings — взаимоисключающи). - if ($attr.PSObject.Properties['planner'] -and $null -ne $attr.planner) { - Emit-PlannerSettings -pl $attr.planner -ind $inner - } - # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart">. - # Тип Settings выводится из типа реквизита (d5p1:GanttChart → d4p1:GanttChart). - if ($attr.PSObject.Properties['chart'] -and $null -ne $attr.chart) { - $ctype = if ("$($attr.type)" -match 'GanttChart') { 'd4p1:GanttChart' } else { 'd4p1:Chart' } - Emit-ChartSettings -chart $attr.chart -ind $inner -ctype $ctype - } - - if ($attr.main -eq $true) { - X "$inner<MainAttribute>true</MainAttribute>" - } - # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) - if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner } - if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner } - $mainSaved = $false - if ($attr.main -eq $true -and $attr.type) { - $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' - } - # Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто. - $emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved } - if ($emitSaved) { - X "$inner<SavedData>true</SavedData>" - } - # Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>; - # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). - # Нет ключа или false → не эмитим. - if ($null -ne $attr.PSObject.Properties['save'] -and $null -ne $attr.save) { - $saveFields = New-Object System.Collections.ArrayList - if ($attr.save -is [bool]) { - if ($attr.save) { [void]$saveFields.Add($attrName) } - } else { - foreach ($e in @($attr.save)) { - $fld = "$e" - if ([string]::IsNullOrEmpty($fld)) { continue } - if ($fld -ne $attrName -and $fld -notmatch '\.' -and $fld -notmatch '^\d+/\d+:') { $fld = "$attrName.$fld" } - if (-not $saveFields.Contains($fld)) { [void]$saveFields.Add($fld) } - } - } - if ($saveFields.Count -gt 0) { - X "$inner<Save>" - foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" } - X "$inner</Save>" - } - } - # Проверка заполнения реквизита → <FillCheck> (реальный тег; <FillChecking> в схеме нет). - # bool true → ShowError (единственное значение в корпусе); строка → verbatim. Синоним fillChecking. - $fcRaw = if ($null -ne $attr.PSObject.Properties['fillCheck']) { $attr.fillCheck } elseif ($null -ne $attr.PSObject.Properties['fillChecking']) { $attr.fillChecking } else { $null } - if ($fcRaw) { - $fcv = if ($fcRaw -is [bool]) { 'ShowError' } else { "$fcRaw" } - X "$inner<FillCheck>$fcv</FillCheck>" - } - - # UseAlways: поля, всегда читаемые (дин-список/таблица). Две формы DSL сливаются: - # attr.useAlways[] (короткие имена) + columns с useAlways:true → <Field>ИмяРеквизита.Поле</Field>. - $uaFields = New-Object System.Collections.ArrayList - if ($attr.useAlways) { - foreach ($e in @($attr.useAlways)) { - $fld = "$e" - # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" - # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. - # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). - if ($fld.StartsWith('~')) { - $bare = $fld.Substring(1) - if ($bare -notmatch "^$([regex]::Escape($attrName))\.") { $bare = "$attrName.$bare" } - $fld = "~$bare" - } elseif ($fld -notmatch "^$([regex]::Escape($attrName))\.") { - $fld = "$attrName.$fld" - } - if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } - } - } - if ($attr.columns) { - foreach ($col in $attr.columns) { - if ($col.useAlways -eq $true) { - $fld = "$attrName.$($col.name)" - if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } - } - } - } - if ($uaFields.Count -gt 0) { - X "$inner<UseAlways>" - foreach ($f in $uaFields) { X "$inner`t<Field>$f</Field>" } - X "$inner</UseAlways>" - } - - Emit-FunctionalOptions -fo $attr.functionalOptions -indent $inner - - # Columns: прямые <Column> (ValueTable/Tree) + <AdditionalColumns table="X"> (доп. колонки - # табличных частей объекта). Порядок схемы: прямые сначала, затем AdditionalColumns-группы. - # Для дин-списка (есть settings) прямые колонки НЕ эмитим (служат лишь для UseAlways). - $hasDirectCols = $attr.columns -and $attr.columns.Count -gt 0 -and -not $attr.settings - $hasAddCols = $attr.additionalColumns -and @($attr.additionalColumns).Count -gt 0 - if ($hasDirectCols -or $hasAddCols) { - X "$inner<Columns>" - if ($hasDirectCols) { - $seenCols = @{} # колонки уникальны в пределах своего реквизита - foreach ($col in $attr.columns) { - Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'" - Emit-AttrColumn -col $col -indent "$inner`t" - } - } - if ($hasAddCols) { - foreach ($ac in @($attr.additionalColumns)) { - $acCols = @($ac.columns) - if ($acCols.Count -eq 0) { - # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) - X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>" - continue - } - X "$inner`t<AdditionalColumns table=`"$($ac.table)`">" - $seenAcCols = @{} # уникальность в пределах группы AdditionalColumns - foreach ($col in $acCols) { - Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'" - Emit-AttrColumn -col $col -indent "$inner`t`t" - } - X "$inner`t</AdditionalColumns>" - } - } - X "$inner</Columns>" - } - - # Settings (динамический список) - if ($attr.settings) { - $st = $attr.settings - X "$inner<Settings xsi:type=`"DynamicList`">" - $si = "$inner`t" - # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings - # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). - if ($null -ne $st.autoFillAvailableFields) { X "$si<AutoFillAvailableFields>$(if ($st.autoFillAvailableFields){'true'}else{'false'})</AutoFillAvailableFields>" } - $hasQuery = $st.query -and "$($st.query)".Trim() - $mq = if ($hasQuery -or $st.manualQuery -eq $true) { "true" } else { "false" } - X "$si<ManualQuery>$mq</ManualQuery>" - # DynamicDataRead: дефолт true; false только при явном отключении - $ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" } - X "$si<DynamicDataRead>$ddr</DynamicDataRead>" - if ($hasQuery) { - $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir - X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>" - } - # Явные поля набора (редко): override title/dataPath - if ($st.fields) { - foreach ($fld in $st.fields) { - X "$si<Field xsi:type=`"dcssch:DataSetFieldField`">" - $dp = if ($fld.dataPath) { $fld.dataPath } else { $fld.field } - X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" - X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" - if ($fld.title) { - X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">" - Emit-MLItems -val $fld.title -indent "$si`t`t" - X "$si`t</dcssch:title>" - } - X "$si</Field>" - } - } - # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. - Emit-DLParameters -params $st.parameters -indent $si - if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" } - # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). - if ($null -ne $st.autoSaveUserSettings) { X "$si<AutoSaveUserSettings>$(if ($st.autoSaveUserSettings){'true'}else{'false'})</AutoSaveUserSettings>" } - # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. - # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. - $lsi = "$si`t" - X "$si<ListSettings>" - if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) { - # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. - # meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present. - foreach ($prop in $st.listSettings.PSObject.Properties) { - $tag = $prop.Name; $meta = "$($prop.Value)" - $bvm = if ($meta -match 'v') { 'Normal' } else { $null } - switch ($tag) { - 'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } - 'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } - 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } - 'itemsViewMode' { X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" } - 'itemsUserSettingID' { X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" } - } - } - } else { - # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. - Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID - # dataParameters — после filter, до order (XSD-порядок ListSettings) - if ($st.PSObject.Properties['dataParameters']) { Emit-DataParameters -items $st.dataParameters -indent $lsi } - Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID - Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID - X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" - X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" - } - X "$si</ListSettings>" - X "$inner</Settings>" - } - - X "$indent`t</Attribute>" - } - # Условное оформление формы — последний child <Attributes> (та же DCS-грамматика, что settings CA) - Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' - X "$indent</Attributes>" -} - -# --- 9. Parameter emitter --- - -function Emit-Parameters { - param($params, [string]$indent) - - if (-not $params -or $params.Count -eq 0) { return } - - X "$indent<Parameters>" - $seenParams = @{} - foreach ($param in $params) { - Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter' - X "$indent`t<Parameter name=`"$($param.name)`">" - $inner = "$indent`t`t" - - Emit-Type -typeStr "$($param.type)" -indent $inner - - if ($param.key -eq $true) { - X "$inner<KeyParameter>true</KeyParameter>" - } - - X "$indent`t</Parameter>" - } - X "$indent</Parameters>" -} - -# --- 10. Command emitter --- - -function Emit-Commands { - param($cmds, [string]$indent) - - if (-not $cmds -or $cmds.Count -eq 0) { return } - - X "$indent<Commands>" - $seenCmds = @{} - foreach ($cmd in $cmds) { - $cmdId = New-Id - Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command' - X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">" - $inner = "$indent`t`t" - - # Заголовок команды (зеркало Emit-Title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс - # (в оригинале <Title> нет — не додумывать); ключ отсутствует → авто-вывод из имени (помощь модели). - if ($null -ne $cmd.PSObject.Properties['title']) { - if ($cmd.title) { Emit-MLText -tag "Title" -text $cmd.title -indent $inner } - } else { - $cmdTitle = Title-FromName -name "$($cmd.name)" - if ($cmdTitle) { Emit-MLText -tag "Title" -text $cmdTitle -indent $inner } - } - - if ($cmd.tooltip) { - Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner - } - - # Доступность команды по ролям (после ToolTip, до Action) - if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner } - - if ($cmd.action) { - X "$inner<Action>$($cmd.action)</Action>" - } - - if ($cmd.modifiesSavedData -eq $true) { X "$inner<ModifiesSavedData>true</ModifiesSavedData>" } - - Emit-FunctionalOptions -fo $cmd.functionalOptions -indent $inner - - if ($cmd.currentRowUse) { - X "$inner<CurrentRowUse>$($cmd.currentRowUse)</CurrentRowUse>" - } - - # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). - # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) - $cmdTable = $cmd.table - if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId } - if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица } - if ($cmdTable) { - X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>" - } - - if ($cmd.shortcut) { - X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" - } - - Emit-CommandPicture -pic $cmd.picture -elemLt $cmd.loadTransparent -indent $inner - - if ($cmd.representation) { - X "$inner<Representation>$($cmd.representation)</Representation>" - } - - X "$indent`t</Command>" - } - X "$indent</Commands>" -} - -# Резолв ключа-группы древовидной формы → CommandGroup (зависит от панели). Дружелюбные -# алиасы стандартных form-групп; любой иной ключ (CommandGroup.X / GUID / "") — verbatim. -function Resolve-CommandGroupKey { - param([string]$key, [string]$panelTag) - $k = ($key -replace '\s','').ToLower() - if ($panelTag -eq 'NavigationPanel') { - switch ($k) { - 'important' { return 'FormNavigationPanelImportant' } - 'важное' { return 'FormNavigationPanelImportant' } - 'goto' { return 'FormNavigationPanelGoTo' } - 'перейти' { return 'FormNavigationPanelGoTo' } - 'seealso' { return 'FormNavigationPanelSeeAlso' } - 'смтакже' { return 'FormNavigationPanelSeeAlso' } - } - } else { - switch ($k) { - 'important' { return 'FormCommandBarImportant' } - 'важное' { return 'FormCommandBarImportant' } - 'createbasedon' { return 'FormCommandBarCreateBasedOn' } - 'создатьнаосновании' { return 'FormCommandBarCreateBasedOn' } - } - } - return $key # verbatim -} - -# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. -# Значение панели: МАССИВ (плоская форма; элемент может нести group) ИЛИ ОБЪЕКТ -# (древовидная форма: {группа: [команды]}, group берётся из ключа, элементы его не дублируют). -# Элемент: строка (голый command, Type=Auto) или объект. Порядок тегов: -# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). -function Emit-CommandInterface { - param($ci, [string]$indent) - if (-not $ci) { return } - $inner = "$indent`t" - $panels = @( - @{ Tag='CommandBar'; Syns=@('commandBar','команднаяПанель','КоманднаяПанель') }, - @{ Tag='NavigationPanel'; Syns=@('navigationPanel','панельНавигации','ПанельНавигации') } - ) - $present = @() - foreach ($p in $panels) { - $items = $null - foreach ($syn in $p.Syns) { if ($null -ne $ci.PSObject.Properties[$syn]) { $items = $ci.($syn); break } } - if ($null -ne $items) { $present += ,@{ Tag=$p.Tag; Items=$items } } - } - if ($present.Count -eq 0) { return } - X "$indent<CommandInterface>" - foreach ($p in $present) { - X "$inner<$($p.Tag)>" - # Нормализация: плоский список пар (элемент, group-из-дерева). Объект → дерево. - $flat = New-Object System.Collections.ArrayList - if ($p.Items -is [System.Management.Automation.PSCustomObject]) { - foreach ($prop in $p.Items.PSObject.Properties) { - $grpFromTree = Resolve-CommandGroupKey -key $prop.Name -panelTag $p.Tag - foreach ($it in @($prop.Value)) { [void]$flat.Add(@{ item=$it; treeGroup=$grpFromTree }) } - } - } else { - foreach ($it in @($p.Items)) { [void]$flat.Add(@{ item=$it; treeGroup=$null }) } - } - foreach ($fi in $flat) { - $item = $fi.item; $treeGroup = $fi.treeGroup - if ($item -is [string]) { - $cmd = $item; $type = 'Auto'; $attr = $null; $grp = $null; $idx = $null; $dv = $null; $vis = $null - } else { - $cmd = Get-ElProp $item @('command','команда') - $type = Get-ElProp $item @('type','тип'); if (-not $type) { $type = 'Auto' } - $attr = Get-ElProp $item @('attribute','реквизит') - $grp = Get-ElProp $item @('group','группа','группаКоманд') - $idx = Get-ElProp $item @('index','индекс') - $dv = Get-ElProp $item @('defaultVisible','видимость','видимостьПоУмолчанию') - $vis = Get-ElProp $item @('visible','видимостьПоРолям','настройкаВидимости') - } - # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк - if ($treeGroup) { $grp = $treeGroup } - X "$inner`t<Item>" - X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>" - X "$inner`t`t<Type>$type</Type>" - if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" } - if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" } - if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" } - if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" } - if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" } - X "$inner`t</Item>" - } - X "$inner</$($p.Tag)>" - } - X "$indent</CommandInterface>" -} - -# --- 11. Properties emitter --- - -function Emit-Properties { - param($props, [string]$indent) - - if (-not $props) { return } - - # camelCase -> PascalCase mapping for known properties - $propMap = @{ - "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" - } - - foreach ($p in $props.PSObject.Properties) { - $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { - # Auto PascalCase: first letter uppercase - $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) - } - # Convert boolean to lowercase string (PS renders as True/False) - $val = $p.Value - # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) - if ($val -is [string] -and $val -eq '') { continue } - if ($val -is [bool]) { - $val = if ($val) { "true" } else { "false" } - } - X "$indent<$xmlName>$val</$xmlName>" - } -} - -# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- - -# Companion-панели как СВОЙСТВА элемента (значение объект/массив): любой знакомый -# синоним → каноника commandBar / contextMenu. Разводим с одноимёнными ТИПАМИ-элементами -# по типу значения: строка = элемент-тип (имя), объект/массив = панель-свойство. -function Normalize-PanelSynonyms { - param($el) - if ($null -eq $el) { return } - $panelSyns = @{ - 'commandBar' = @('commandBar','autoCommandBar','AutoCommandBar','autoCmdBar','cmdBar','КоманднаяПанель') - 'contextMenu' = @('contextMenu','ContextMenu','КонтекстноеМеню') - } - foreach ($canon in $panelSyns.Keys) { - foreach ($syn in $panelSyns[$canon]) { - $p = $el.PSObject.Properties[$syn] - if ($null -ne $p -and ($p.Value -is [array] -or $p.Value -is [System.Management.Automation.PSCustomObject])) { - if ($syn -ne $canon -and $null -eq $el.PSObject.Properties[$canon]) { - $v = $p.Value - $el.PSObject.Properties.Remove($syn) | Out-Null - $el | Add-Member -NotePropertyName $canon -NotePropertyValue $v -Force - } - break - } - } - } -} - -function Normalize-ElementSynonyms { - param($el) - if ($null -eq $el) { return } - Normalize-PanelSynonyms $el - # Тип-синонимы (commandBar/autoCommandBar → элемент-тип) применяем ТОЛЬКО к строковому - # значению (имя элемента); объект/массив уже отнесён к панель-свойству выше. - $typeSyn = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" } - foreach ($pair in $typeSyn.GetEnumerator()) { - $src = $el.PSObject.Properties[$pair.Key] - if ($null -ne $src -and ($src.Value -is [string]) -and $null -eq $el.PSObject.Properties[$pair.Value]) { - $val = $el.($pair.Key) - $el.PSObject.Properties.Remove($pair.Key) | Out-Null - $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force - } - } - if ($el.PSObject.Properties["extTooltip"] -and $null -eq $el.PSObject.Properties["extendedTooltip"]) { - $val = $el.extTooltip - $el.PSObject.Properties.Remove("extTooltip") | Out-Null - $el | Add-Member -NotePropertyName "extendedTooltip" -NotePropertyValue $val -Force - } - # Рекурсия в детей панелей (commandBar/contextMenu) — нормализуем кнопки/группы внутри - foreach ($pk in @('commandBar','contextMenu')) { - $pp = $el.PSObject.Properties[$pk] - if ($null -ne $pp) { - $kids = if ($pp.Value -is [array]) { $pp.Value } elseif ($null -ne $pp.Value) { $pp.Value.children } else { $null } - if ($kids) { foreach ($child in $kids) { Normalize-ElementSynonyms $child } } - } - } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { Normalize-ElementSynonyms $child } - } - if ($el.PSObject.Properties["columns"] -and $el.columns) { - foreach ($child in $el.columns) { Normalize-ElementSynonyms $child } - } -} - -function HasCmdBarRecursive { - param($el) - if ($null -eq $el) { return $false } - if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } } - } - if ($el.PSObject.Properties["columns"] -and $el.columns) { - foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } } - } - return $false -} - -function ApplyDynamicListTableHeuristic { - param($el, [string]$listName, [bool]$hasMainTable) - if ($null -eq $el) { return } - if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) { - # Маркер дин-список-таблицы → Emit-Table эмитит блок свойств (Group A defaults) - $el | Add-Member -NotePropertyName "_dynList" -NotePropertyValue $true -Force - if ($null -eq $el.PSObject.Properties["tableAutofill"]) { - $el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force - } - if ($null -eq $el.PSObject.Properties["commandBarLocation"]) { - $el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force - } - # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. - # Декомпилятор опускает ключ при rpdp == smart-default (ждёт реинъекции); реальное отсутствие - # фиксирует ""-маркером (НЕ перезатирается). Гейт hasMainTable снят: дин-список без mainTable - # (напр. query-based) тоже несёт RowPictureDataPath. - if ($null -eq $el.PSObject.Properties["rowPictureDataPath"]) { - $el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force - } - } - if ($el.PSObject.Properties["children"] -and $el.children) { - foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable } - } -} - -function Test-IsObjectLikeType { - param([string]$type) - if ([string]::IsNullOrEmpty($type)) { return $false } - if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true } - $objectSuffixes = @( - "CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject", - "ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject", - "TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject", - "ChartOfCalculationTypesObject", "ExchangePlanObject" - ) - $recordSetPrefixes = @( - "InformationRegisterRecordSet", "AccumulationRegisterRecordSet", - "AccountingRegisterRecordSet", "CalculationRegisterRecordSet", - "InformationRegisterRecordManager" - ) - foreach ($suffix in $objectSuffixes) { - if ($type -like "$suffix.*") { return $true } - } - foreach ($prefix in $recordSetPrefixes) { - if ($type -like "$prefix.*") { return $true } - } - return $false -} - -# 11b.1: Normalize synonyms recursively -if ($def.elements) { - foreach ($el in $def.elements) { Normalize-ElementSynonyms $el } -} - -# 11b.2: Extract autoCmdBar element from def.elements -$script:mainAcbDef = $null -if ($def.elements) { - $autoBars = @() - $rest = @() - foreach ($el in $def.elements) { - if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) { - $autoBars += $el - } else { - $rest += $el - } - } - if ($autoBars.Count -gt 1) { - Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed." - exit 1 - } - if ($autoBars.Count -eq 1) { - $script:mainAcbDef = $autoBars[0] - # Replace def.elements with the filtered list - $def.PSObject.Properties.Remove("elements") | Out-Null - $def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force - } -} - -# 11b.3: Infer main attribute (only if no attribute has main:true) -if ($def.attributes) { - $hasExplicitMain = $false - foreach ($attr in $def.attributes) { - if ($attr.main -eq $true) { $hasExplicitMain = $true; break } - } - if (-not $hasExplicitMain) { - $candidates = @() - foreach ($attr in $def.attributes) { - # Skip if user explicitly opted out via main:false - if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue } - if (Test-IsObjectLikeType "$($attr.type)") { - $candidates += $attr - } - } - if ($candidates.Count -eq 1) { - $candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force - Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))" - } elseif ($candidates.Count -gt 1) { - $names = ($candidates | ForEach-Object { $_.name }) -join ", " - Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly" - } - } -} - -# 11b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) -if ($def.attributes -and $def.elements) { - foreach ($attr in $def.attributes) { - if ("$($attr.type)" -ne "DynamicList") { continue } - $mt = $null - if ($attr.PSObject.Properties["settings"] -and $null -ne $attr.settings) { - if ($attr.settings -is [hashtable]) { - if ($attr.settings.ContainsKey("mainTable")) { $mt = $attr.settings["mainTable"] } - } elseif ($attr.settings.PSObject.Properties["mainTable"]) { - $mt = $attr.settings.mainTable - } - } - $hasMt = -not [string]::IsNullOrEmpty("$mt") - foreach ($el in $def.elements) { - ApplyDynamicListTableHeuristic $el $attr.name $hasMt - } - } -} - -# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3 -function Compute-MainAcbAutofill { - if ($script:mainAcbDef) { - if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) { - return [bool]$script:mainAcbDef.autofill - } - return $true - } - if ($def.elements) { - foreach ($el in $def.elements) { - if (HasCmdBarRecursive $el) { return $false } - } - } - return $true -} - -# --- 12. Main compilation --- - -# Title -if ($def.title) { - Emit-MLText -tag "Title" -text $def.title -indent "`t" -} - -# Header -X '<?xml version="1.0" encoding="UTF-8"?>' -X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" - -# Oops — Title was emitted before header. Need to fix the order. -# Actually, let me restructure: build the body into a separate buffer, then assemble - -# Reset and rebuild properly -$script:xml = New-Object System.Text.StringBuilder 8192 -$script:nextId = 1 -$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме) - -X '<?xml version="1.0" encoding="UTF-8"?>' -X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" - -# 12a. Title (from def.title or properties.title — must be multilingual XML) -$formTitle = $def.title -if (-not $formTitle -and $def.properties -and $def.properties.title) { - $formTitle = $def.properties.title -} -if ($formTitle) { - Emit-MLText -tag "Title" -text $formTitle -indent "`t" -} - -# 12b. Properties (skip 'title' — handled above as multilingual) -# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; -# otherwise platform appends synonym → "Title: Synonym" double-titles). -$propsClone = New-Object PSObject -$hasAutoTitle = $false -if ($def.properties) { - foreach ($p in $def.properties.PSObject.Properties) { - if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true } - } -} -if ($formTitle -and -not $hasAutoTitle) { - $propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false -} -if ($def.properties) { - foreach ($p in $def.properties.PSObject.Properties) { - if ($p.Name -ne "title") { - $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value - } - } -} -Emit-Properties -props $propsClone -indent "`t" - -# 12c. CommandSet (excluded commands) -if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { - X "`t<CommandSet>" - foreach ($cmd in $def.excludedCommands) { - X "`t`t<ExcludedCommand>$cmd</ExcludedCommand>" - } - X "`t</CommandSet>" -} - -# 12c2. MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок -# (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). -if ($def.mobileCommandBarContent -and @($def.mobileCommandBarContent).Count -gt 0) { - X "`t<MobileDeviceCommandBarContent>" - foreach ($nm in @($def.mobileCommandBarContent)) { - X "`t`t<xr:Item>" - X "`t`t`t<xr:Presentation/>" - X "`t`t`t<xr:CheckState>0</xr:CheckState>" - X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" - X "`t`t</xr:Item>" - } - X "`t</MobileDeviceCommandBarContent>" -} - -# 12d. AutoCommandBar (always present, id=-1) -$acbAutofill = Compute-MainAcbAutofill -$acbName = "ФормаКоманднаяПанель" -$acbHAlign = $null -if ($script:mainAcbDef) { - if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") { - $acbName = "$($script:mainAcbDef.autoCmdBar)" - } - if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") { - $acbName = "$($script:mainAcbDef.name)" - } - if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") { - $acbHAlign = "$($script:mainAcbDef.horizontalAlign)" - } -} -$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0) -$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren) -if ($acbHasInner) { - X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">" - if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" } - if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" } - if ($hasAcbChildren) { - X "`t`t<ChildItems>" - foreach ($child in $script:mainAcbDef.children) { - Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true - } - X "`t`t</ChildItems>" - } - X "`t</AutoCommandBar>" -} else { - X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"/>" -} - -# 12e. Events -if ($def.events) { - foreach ($p in $def.events.PSObject.Properties) { - if ($script:knownFormEvents -notcontains $p.Name) { - Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" - } - } - X "`t<Events>" - foreach ($p in $def.events.PSObject.Properties) { - X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>" - } - X "`t</Events>" -} - -# 12f. ChildItems (elements) -if ($def.elements -and $def.elements.Count -gt 0) { - X "`t<ChildItems>" - foreach ($el in $def.elements) { - Emit-Element -el $el -indent "`t`t" - } - X "`t</ChildItems>" -} - -# 12g. Attributes -Emit-Attributes -attrs $def.attributes -indent "`t" -conditionalAppearance $def.conditionalAppearance - -# 12h. Parameters -Emit-Parameters -params $def.parameters -indent "`t" - -# 12i. Commands -Emit-Commands -cmds $def.commands -indent "`t" - -# 12i2. CommandInterface (командный интерфейс формы — последний дочерний Form) -Emit-CommandInterface -ci $def.commandInterface -indent "`t" - -# 12j. Close -X '</Form>' - -# --- 13. Write output --- - -$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } -$outDir = [System.IO.Path]::GetDirectoryName($outPath) -if (-not (Test-Path $outDir)) { - New-Item -ItemType Directory -Path $outDir -Force | Out-Null -} - -$enc = New-Object System.Text.UTF8Encoding($true) -[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) - -# --- 13b. Auto-register form in parent object XML --- - -# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml -$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) -$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) -$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) -$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) -$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) - -$formName = [System.IO.Path]::GetFileName($formNameDir) -$objectName = [System.IO.Path]::GetFileName($objectDir) -$formsLeaf = [System.IO.Path]::GetFileName($formsDir) - -if ($formsLeaf -eq 'Forms') { - $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" - if (Test-Path $objectXmlPath) { - $objDoc = New-Object System.Xml.XmlDocument - $objDoc.PreserveWhitespace = $true - $objDoc.Load($objectXmlPath) - - $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) - $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") - - $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) - if ($childObjects) { - $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) - if (-not $existing) { - $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") - $formElem.InnerText = $formName - - $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) - if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } - - if ($insertBefore) { - $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null - $ws = $objDoc.CreateWhitespace("`n`t`t`t") - $childObjects.InsertBefore($ws, $insertBefore) | Out-Null - } else { - $lastChild = $childObjects.LastChild - if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { - $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null - $childObjects.InsertBefore($formElem, $lastChild) | Out-Null - } else { - $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null - $childObjects.AppendChild($formElem) | Out-Null - $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null - } - } - - $regEnc = New-Object System.Text.UTF8Encoding($true) - $regSettings = New-Object System.Xml.XmlWriterSettings - $regSettings.Encoding = $regEnc - $regSettings.Indent = $false - $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) - $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) - $objDoc.Save($regWriter) - $regWriter.Close() - $regStream.Close() - - Write-Host " Registered: <Form>$formName</Form> in $objectName.xml" - } - } - } -} - -# --- 14. Summary --- - -$elCount = $script:nextId - 1 -Write-Host "[OK] Compiled: $OutputPath" -Write-Host " Elements+IDs: $elCount" -if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } -if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } -if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } +# form-compile v1.138 — Compile 1C managed form from JSON or object metadata +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [string]$JsonPath, + + [Parameter(Mandatory)] + [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 field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag) + $extractFields = { + param($parentNode, [string]$tagName) + $result = @() + if (-not $parentNode) { return $result } + foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) { + $fp = $fieldNode.SelectSingleNode("md:Properties", $ns) + $fName = $fp.SelectSingleNode("md:Name", $ns).InnerText + $fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns) + $fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName } + $fTypeNode = $fp.SelectSingleNode("md:Type", $ns) + $fType = & $extractType $fTypeNode + $result += @{ + Name = $fName + Synonym = $fSyn + Type = $fType + IsRef = (& $isRefType $fType) + } + } + return $result + } + + # Attributes + $attributes = @(& $extractFields $childObjs "Attribute") + + # 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 = @(& $extractFields $tsCo "Attribute") + $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 + } + "InformationRegister" { + $meta.Dimensions = @(& $extractFields $childObjs "Dimension") + $meta.Resources = @(& $extractFields $childObjs "Resource") + $prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns) + $meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" } + $wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns) + $meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" } + } + "AccumulationRegister" { + $meta.Dimensions = @(& $extractFields $childObjs "Dimension") + $meta.Resources = @(& $extractFields $childObjs "Resource") + $rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns) + $meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" } + } + "ChartOfCharacteristicTypes" { + $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 = @() + foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) { + $owners += $ow.InnerText + } + $meta.Owners = $owners + $meta.HasValueType = $true + } + "ExchangePlan" { + $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 } + $meta.Hierarchical = $false + $meta.HierarchyType = $null + $meta.Owners = @() + } + "ChartOfAccounts" { + $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 } + $meta.Hierarchical = $true + $htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns) + $meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" } + $meta.Owners = @() + $maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns) + $meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 } + $meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag") + $meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag") + } + } + + 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" } + } + # ─── Register defaults ─── + "informationRegister.record" = @{ + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "informationRegister.list" = @{ + columns = "all"; columnType = "labelField" + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + "accumulationRegister.list" = @{ + columns = "all"; columnType = "labelField" + tableCommandBar = "none"; commandBar = "auto" + properties = @{} + } + # ─── Catalog-like type defaults ─── + "chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" } + "chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" } + "chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" } + "chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" } + "exchangePlan.item" = @{ basedOn = "catalog.item" } + "exchangePlan.list" = @{ basedOn = "catalog.list" } + "exchangePlan.choice" = @{ basedOn = "catalog.choice" } + # ─── ChartOfAccounts defaults ─── + "chartOfAccounts.item" = @{ + parent = @{ title = "Подчинен счету" } + fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } + properties = @{} + } + "chartOfAccounts.folder" = @{ + parent = @{ title = "Подчинен счету" } + properties = @{ windowOpeningMode = "LockOwnerWindow" } + } + "chartOfAccounts.list" = @{ basedOn = "catalog.list" } + "chartOfAccounts.choice" = @{ basedOn = "catalog.choice" } + } + + # 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 --- +# Non-displayable types — cannot be bound to form elements +$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения') + +function Test-DisplayableType([string]$typeStr) { + foreach ($nd in $script:nonDisplayableTypes) { + if ($typeStr -match [regex]::Escape($nd)) { return $false } + } + return $true +} + +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 } + + # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. + # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы + # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) + + # 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) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $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"; userVisible = $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 } + if (-not (Test-DisplayableType $attr.Type)) { 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 = @() + # Standard columns: Number + Date + $columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" } + $columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" } + # All custom attributes as labelField + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $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"; userVisible = $false } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + 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) -and (Test-DisplayableType $attr.Type)) { $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 -and (Test-DisplayableType $fAttr.Type)) { + $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 } + ) + } +} + +# ─── InformationRegister ────────────────────────────────────────────────── + +function Generate-InformationRegisterDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "informationRegister.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } + switch ($purpose) { + "Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd } + "List" { return Generate-InformationRegisterListDSL $meta $p } + } +} + +function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) { + $elements = @() + $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" + + # Period first (if periodic) + if ($isPeriodic) { + $elements += [ordered]@{ input = "Период"; path = "Запись.Period" } + } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd) + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd) + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) + } + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = $elements + attributes = @( + @{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-InformationRegisterListDSL($meta, [hashtable]$p) { + $isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical" + $isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate" + + $columns = @() + # Period + if ($isPeriodic) { + $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } + } + # Recorder/LineNumber for subordinate registers + if ($isRecorderSubordinate) { + $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } + $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } + } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elKey = "labelField" + if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elKey = "labelField" + if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = @($tableEl) + attributes = @( + @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } } + ) + } +} + +# ─── AccumulationRegister ───────────────────────────────────────────────── + +function Generate-AccumulationRegisterDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "accumulationRegister.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + switch ($purpose) { + "List" { return Generate-AccumulationRegisterListDSL $meta $p } + } +} + +function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) { + $columns = @() + # AccumulationRegisters always have Period, Recorder, LineNumber + $columns += [ordered]@{ labelField = "Период"; path = "Список.Period" } + $columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" } + $columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" } + # Dimensions + foreach ($dim in $meta.Dimensions) { + if (-not (Test-DisplayableType $dim.Type)) { continue } + $columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" } + } + # Resources + foreach ($res in $meta.Resources) { + if (-not (Test-DisplayableType $res.Type)) { continue } + $elKey = "labelField" + if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" } + } + # Attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elKey = "labelField" + if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" } + $columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" } + } + + $tableEl = [ordered]@{ + table = "Список"; path = "Список" + rowPictureDataPath = "Список.DefaultPicture" + commandBarLocation = "None" + tableAutofill = $false + columns = $columns + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = @($tableEl) + attributes = @( + @{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } } + ) + } +} + +# ─── ChartOfCharacteristicTypes (delegates to Catalog) ──────────────────── + +function Generate-ChartOfCharacteristicTypesDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + # Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc. + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose + + # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types + $catObjType = "CatalogObject.$($meta.Name)" + $ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)" + $catListType = "Catalog.$($meta.Name)" + $ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)" + + foreach ($a in $dsl.attributes) { + if ($a.type -eq $catObjType) { $a.type = $ccoctObjType } + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { + $a.settings.mainTable = $ccoctListType + } + } + + # For Item forms: inject ValueType field after Code/Description + if ($purpose -eq "Item" -and $dsl.elements) { + $vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" } + $newElements = @() + $inserted = $false + foreach ($el in $dsl.elements) { + $newElements += $el + if (-not $inserted) { + $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } + if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { + $newElements += $vtEl + $inserted = $true + } + } + } + if (-not $inserted) { $newElements += $vtEl } + $dsl.elements = $newElements + } + + return $dsl +} + +# ─── ExchangePlan (delegates to Catalog) ────────────────────────────────── + +function Generate-ExchangePlanDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + # ExchangePlans are not hierarchical and have no Folder form + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose + + # Post-patch: replace Catalog types with ExchangePlan types + $catObjType = "CatalogObject.$($meta.Name)" + $epObjType = "ExchangePlanObject.$($meta.Name)" + $catListType = "Catalog.$($meta.Name)" + $epListType = "ExchangePlan.$($meta.Name)" + + foreach ($a in $dsl.attributes) { + if ($a.type -eq $catObjType) { $a.type = $epObjType } + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) { + $a.settings.mainTable = $epListType + } + } + + # For Item forms: inject SentNo, ReceivedNo after Code/Description + if ($purpose -eq "Item" -and $dsl.elements) { + $sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true } + $recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true } + $newElements = @() + $inserted = $false + foreach ($el in $dsl.elements) { + $newElements += $el + if (-not $inserted) { + $elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" } + if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") { + $newElements += $sentEl + $newElements += $recvEl + $inserted = $true + } + } + } + if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl } + $dsl.elements = $newElements + } + + return $dsl +} + +# ─── ChartOfAccounts ────────────────────────────────────────────────────── + +function Generate-ChartOfAccountsDSL { + param($meta, [hashtable]$presetData, [string]$purpose) + $pKey = "chartOfAccounts.$($purpose.ToLower())" + $p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} } + $fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } } + switch ($purpose) { + "Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData } + "Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p } + "List" { return Generate-ChartOfAccountsListDSL $meta $presetData } + "Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData } + } +} + +function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) { + $elements = @() + + # Header: Code + Parent + $headerLeftChildren = @() + if ($meta.CodeLength -gt 0) { + $headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + $headerRightChildren = @() + if ($meta.Hierarchical) { + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } + $headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } + } + + if ($headerRightChildren.Count -gt 0) { + $elements += [ordered]@{ + group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none" + children = @( + [ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren } + [ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren } + ) + } + } elseif ($headerLeftChildren.Count -gt 0) { + $elements += $headerLeftChildren + } + + # Description + if ($meta.DescriptionLength -gt 0) { + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + } + + # OffBalance + $elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" } + + # AccountingFlags as checkboxes + if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) { + $flagChildren = @() + foreach ($flag in $meta.AccountingFlags) { + $flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" } + } + $elements += [ordered]@{ + group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета" + children = $flagChildren + } + } + + # ExtDimensionTypes table + if ($meta.MaxExtDimensionCount -gt 0) { + # Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С), + # иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта. + $edTable = "ВидыСубконто" + $edCols = @() + $edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" } + $edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" } + if ($meta.ExtDimensionAccountingFlags) { + foreach ($edFlag in $meta.ExtDimensionAccountingFlags) { + $edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" } + } + } + $elements += [ordered]@{ + table = $edTable + path = "Объект.ExtDimensionTypes" + columns = $edCols + } + } + + # Custom attributes + foreach ($attr in $meta.Attributes) { + if (-not (Test-DisplayableType $attr.Type)) { continue } + $elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd) + } + + # Tabular sections + $tsExclude = @("ДополнительныеРеквизиты","Представления") + foreach ($ts in $meta.TabularSections) { + if ($tsExclude -contains $ts.Name) { continue } + $tsCols = @() + foreach ($col in $ts.Columns) { + if (-not (Test-DisplayableType $col.Type)) { continue } + $tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd) + } + $elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols } + } + + $props = [ordered]@{} + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + properties = $props + elements = $elements + attributes = @( + @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) { + $elements = @() + if ($meta.CodeLength -gt 0) { + $elements += [ordered]@{ input = "Код"; path = "Объект.Code" } + } + if ($meta.DescriptionLength -gt 0) { + $elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" } + } + if ($meta.Hierarchical) { + $parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" } + $elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle } + } + + $props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" } + if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } } + + return [ordered]@{ + title = $meta.Synonym + useForFoldersAndItems = "Folders" + properties = $props + elements = $elements + attributes = @( + @{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true } + ) + } +} + +function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) { + # Delegate to Catalog List and patch types + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List" + foreach ($a in $dsl.attributes) { + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { + $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" + } + } + return $dsl +} + +function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) { + $dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice" + foreach ($a in $dsl.attributes) { + if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") { + $a.settings.mainTable = "ChartOfAccounts.$($meta.Name)" + } + } + return $dsl +} + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + +# --- Detect XML format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$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)) + +# --- 0. Path normalization and mode dispatch --- + +# Form name → purpose mapping +$script:formNameToPurpose = @{ + "ФормаДокумента" = "Item" + "ФормаЭлемента" = "Item" + "ФормаСписка" = "List" + "ФормаВыбора" = "Choice" + "ФормаГруппы" = "Folder" + "ФормаЗаписи" = "Record" + "ФормаСчета" = "Item" + "ФормаУзла" = "Item" +} + +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 +} + +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") } + "InformationRegister" { @("Record","List") } + "AccumulationRegister" { @("List") } + "ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") } + "ExchangePlan" { @("Item","List","Choice") } + "ChartOfAccounts" { @("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, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts." + 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 } + "InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose } + "ChartOfAccounts" { Generate-ChartOfAccountsDSL -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 +} + +# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile) +$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path } +function Resolve-QueryValue { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath)) + } + foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } + Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + exit 1 +} + +# --- 2. ID allocator --- + +$script:nextId = 1 +function New-Id { + $id = $script:nextId + $script:nextId++ + return $id +} + +# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё +# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. +function Assert-UniqueName { + param([string]$name, [hashtable]$seen, [string]$kind) + if ($seen.ContainsKey($name)) { + Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')" + exit 1 + } + $seen[$name] = $true +} + +# --- 3. XML helper --- + +$script:xml = New-Object System.Text.StringBuilder 8192 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + # Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > . + # Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально); + # " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов. + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>') +} + +# --- 4. Multilang helper --- + +# Эмитит <v8:item> для значения: строка → один ru-элемент; объект {lang:text} → по элементу на язык. +function Emit-MLItems { + param($val, [string]$indent) + if ($val -is [System.Collections.IDictionary]) { + foreach ($k in $val.Keys) { + X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>" + } + } elseif ($val -is [System.Management.Automation.PSCustomObject]) { + foreach ($p in $val.PSObject.Properties) { + X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>" + } + } else { + X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>" + } +} + +function Emit-MLText { + param([string]$tag, $text, [string]$indent, [string]$xsiType) + $attr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } + X "$indent<$tag$attr>" + Emit-MLItems -val $text -indent "$indent`t" + X "$indent</$tag>" +} + +# <dcsset:userSettingPresentation> и подобные DCS-подписи: платформа пишет плоскую строку как +# xsi:type="xs:string" (скаляр; корпус 26), мультиязычный текст — как xsi:type="v8:LocalStringType" +# (7). Декомпилятор различает (Get-PresText: строка ИЛИ объект {ru,en}). +function Emit-USPresentation { + param($val, [string]$tag, [string]$indent) + if ($null -eq $val) { return } + if ($val -is [string]) { + X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)</$tag>" + } else { + Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType" + } +} + +# Детектор «настоящей» inline-разметки форматированного текста (1С: <link>/<b>/<color>/… +# и закрывающий </>). Плейсхолдеры вида <не заполнен> НЕ срабатывают (нет известного тега/</>). +# ВАЖНО: regex должен быть идентичен в form-decompile (иначе гибрид-раундтрип поедет). +$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' +function Test-HasRealMarkup { + param($text) + if ($null -eq $text) { return $false } + $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } + elseif ($text -is [System.Management.Automation.PSCustomObject]) { @($text.PSObject.Properties.Value) } + else { @("$text") } + foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } + return $false +} +# DSL-значение ML-поля → @{ text; formatted }. Форма {text, formatted} = явный override; +# строка/мапа → авто-детект formatted по разметке. +function Resolve-MLFormatted { + param($val) + $hasText = $false + if ($val -is [System.Management.Automation.PSCustomObject]) { $hasText = [bool]$val.PSObject.Properties['text'] } + elseif ($val -is [System.Collections.IDictionary]) { $hasText = $val.Contains('text') } + if ($hasText) { + $t = if ($val -is [System.Collections.IDictionary]) { $val['text'] } else { $val.text } + $f = if ($val -is [System.Collections.IDictionary]) { $val['formatted'] } else { $val.formatted } + return @{ text = $t; formatted = [bool]$f } + } + return @{ text = $val; formatted = (Test-HasRealMarkup $val) } +} + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип +# (harness нормализует GUID для хвоста с иными идентификаторами). +$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + +# ───────────────────────────────────────────────────────────────────────────── +# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. +# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). +# ───────────────────────────────────────────────────────────────────────────── +function New-Guid-String { return [System.Guid]::NewGuid().ToString() } + +$script:comparisonTypes = @{ + "=" = "Equal"; "<>" = "NotEqual" + ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual" + "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "like" = "Like"; "notLike" = "NotLike" + "подобно" = "Like"; "неподобно" = "NotLike" # рус. синоним (хэш регистронезависим: ПОДОБНО=подобно) + "filled" = "Filled"; "notFilled" = "NotFilled" +} + +function Parse-FilterShorthand { + param([string]$s) + $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } + if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } + if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } + if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } + if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } + if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible"; $s = $s -replace '\s*@inaccessible', '' } + $s = $s.Trim() + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notLike\b', 'like\b', 'неподобно\b', 'подобно\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $result.op = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true"); $result["valueType"] = "xs:boolean" } + elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart } # дата без valueType → Emit-FilterItem выведет StandardBeginningDate Custom (дефолт даты в фильтре) + elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart; $result["valueType"] = "xs:decimal" } + elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart; $result["valueType"] = "dcscor:DesignTimeValue" } + else { $result.value = $valPart; $result["valueType"] = "xs:string" } + } + } else { $result.field = $s } + return $result +} + +function Emit-FilterItem { + param($item, [string]$indent) + if ($item.group) { + $groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } } + X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">" + X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>" + if ($item.items) { + foreach ($sub in $item.items) { + if ($sub -is [string]) { + $parsed = Parse-FilterShorthand $sub + $obj = @{ field = $parsed.field; op = $parsed.op } + if ($parsed.use -eq $false) { $obj.use = $false } + if ($null -ne $parsed.value) { $obj.value = $parsed.value } + if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } + if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } + if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } + $sub = [pscustomobject]$obj + } + Emit-FilterItem -item $sub -indent "$indent`t" + } + } + if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } + if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } + if ($item.userSettingID) { + $guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</dcsset:item>" + return + } + X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">" + if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } + X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>" + $compType = $script:comparisonTypes["$($item.op)"] + if (-not $compType) { $compType = "$($item.op)" } + X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>" + $valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string]) + if ($valIsArray) { + if (@($item.value).Count -eq 0) { + X "$indent`t<dcsset:right xsi:type=`"v8:ValueListType`">" + X "$indent`t`t<v8:valueType/>" + X "$indent`t`t<v8:lastId xsi:type=`"xs:decimal`">-1</v8:lastId>" + X "$indent`t</dcsset:right>" + } else { + foreach ($v in $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + if ($v -is [bool]) { $vt = 'xs:boolean' } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = 'xs:decimal' } + elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = 'xs:dateTime' } + elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' } + elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' } + else { $vt = 'xs:string' } + } + $vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" } + X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>" + } + } + } elseif ($null -ne $item.value -and ( + "$($item.valueType)" -match 'Standard(Beginning|End)Date$' -or + (-not $item.valueType -and "$($item.value)" -match '^\d{4}-\d{2}-\d{2}T'))) { + # Стандартная дата начала/окончания. Формы значения: + # объект {variant, date?} — полная (Custom несёт <v8:date>); + # строка-вариант "BeginningOfThisDay" — именованный вариант без даты; + # голая ISO-дата без valueType — шорткат для Custom+date (дата в фильтре платформой + # почти всегда хранится как StandardBeginningDate Custom, корпус 268 vs 2 xs:dateTime; + # явный valueType="xs:dateTime" → плоская дата, ветка ниже). + $sdType = if ($item.valueType) { "$($item.valueType)" -replace '^v8:','' } else { 'StandardBeginningDate' } + $sv = $item.value + if (($sv -is [PSCustomObject]) -or ($sv -is [System.Collections.IDictionary])) { + $variant = if ($sv -is [PSCustomObject]) { "$($sv.variant)" } else { "$($sv['variant'])" } + $hasDate = if ($sv -is [PSCustomObject]) { [bool]$sv.PSObject.Properties['date'] } else { $sv.Contains('date') } + $dateV = if ($hasDate) { if ($sv -is [PSCustomObject]) { "$($sv.date)" } else { "$($sv['date'])" } } else { $null } + } elseif ("$sv" -match '^\d{4}-\d{2}-\d{2}T') { + $variant = 'Custom'; $hasDate = $true; $dateV = "$sv" + } else { + $variant = "$sv"; $hasDate = $false; $dateV = $null + } + X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">" + X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-Xml $variant)</v8:variant>" + if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-Xml $dateV)</v8:date>" } + X "$indent`t</dcsset:right>" + } elseif ("$($item.value)" -eq '_') { + # "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right> + # (напр. <dcsset:right xsi:type="dcscor:Field"/> — сравнение с незаданным полем). + $vt = if ($item.valueType) { "$($item.valueType)" } else { 'xs:string' } + X "$indent`t<dcsset:right xsi:type=`"$vt`"/>" + } elseif ($null -ne $item.value) { + $vt = if ($item.valueType) { "$($item.valueType)" } else { "" } + if (-not $vt) { + $v = $item.value + if ($v -is [bool]) { $vt = "xs:boolean" } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) { $vt = "xs:decimal" } + elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') { $vt = "xs:dateTime" } + elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" } + elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" } + else { $vt = "xs:string" } + } + $vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" } + X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>" + } + if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } + if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } + if ($item.userSettingID) { + $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" } + X "$indent</dcsset:item>" +} + +function Emit-Filter { + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null) + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + X "$indent<dcsset:filter>" + foreach ($item in $items) { + if ($item -is [string]) { + $parsed = Parse-FilterShorthand $item + $obj = @{ field = $parsed.field; op = $parsed.op } + if ($parsed.use -eq $false) { $obj.use = $false } + if ($null -ne $parsed.value) { $obj.value = $parsed.value } + if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] } + if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID } + if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode } + Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t" + } else { Emit-FilterItem -item $item -indent "$indent`t" } + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</dcsset:filter>" +} + +function Emit-Order { + param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null) + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + X "$indent<dcsset:order>" + foreach ($item in $items) { + if ($item -is [string]) { + if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" } } + else { + $parts = $item -split '\s+' + $field = $parts[0] + $dir = "Asc" + if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" } + elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>" + X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" + X "$indent`t</dcsset:item>" + } + } else { + if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" }; continue } + $dir = if ($item.direction) { "$($item.direction)" } else { "Asc" } + if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" } + X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">" + if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" } + X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>" + X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>" + if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" } + X "$indent`t</dcsset:item>" + } + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</dcsset:order>" +} + +function Emit-AppearanceValue { + param([string]$key, $val, [string]$indent) + X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + function _HasKey { param($o, [string]$k) + if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] } + if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) } + return $false + } + function _Get { param($o, [string]$k) + if ($o -is [PSCustomObject]) { return $o.$k } + if ($o -is [System.Collections.IDictionary]) { return $o[$k] } + return $null + } + $isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line') + $useWrapper = $false + $innerVal = $val + $nestedItems = $null + if ($isTopLevelLine) { + if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } + if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } + } elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) { + $innerVal = (_Get $val 'value') + if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true } + if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') } + } + if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" } + X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>" + $isFontDict = $false + if ($innerVal -is [PSCustomObject]) { + $tProp = $innerVal.PSObject.Properties['@type'] + if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true } + } elseif ($innerVal -is [System.Collections.IDictionary]) { + if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true } + } + $isLineDict = $false + if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') } + $isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject]) + if ($isLineDict) { + $lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 } + $lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' } + $ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' } + X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">" + X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>" + X "$indent`t</dcscor:value>" + } elseif ($isFontDict) { + $attrParts = @() + foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $av = $null + if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } } + else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } } + if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" } + } + X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>" + } elseif ($isDict -and (_HasKey $innerVal 'field')) { + # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки + X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-Xml "$(_Get $innerVal 'field')")</dcscor:value>" + } elseif ($isDict) { + # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value + Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType" + } else { + $actualVal = "$innerVal" + $keyTypeMap = @{ + 'Размещение' = 'dcscor:DataCompositionTextPlacementType' + 'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign' + 'ВертикальноеПоложение' = 'v8ui:VerticalAlign' + 'ОриентацияТекста' = 'xs:decimal' + 'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement' + 'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType' + } + $keyType = $keyTypeMap[$key] + if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" } + elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" } + elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" } + elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { + # Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит + # нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше). + # Пустая строка → самозакрывающийся тег (как у платформы). + if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" } + else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" } + } + elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" } + elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" } + else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" } + } + if ($nestedItems) { + $niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null } + if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } } + elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } } + } + X "$indent</dcscor:item>" +} + +function Emit-ConditionalAppearance { + param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance') + $hasItems = $items -and $items.Count -gt 0 + $hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) + if (-not $hasItems -and -not $hasBlockMeta) { return } + X "$indent<$wrapTag>" + foreach ($ca in $items) { + X "$indent`t<dcsset:item>" + if ($ca.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" } + if ($ca.selection -and $ca.selection.Count -gt 0) { + X "$indent`t`t<dcsset:selection>" + foreach ($sel in $ca.selection) { + X "$indent`t`t`t<dcsset:item>" + X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>" + X "$indent`t`t`t</dcsset:item>" + } + X "$indent`t`t</dcsset:selection>" + } else { X "$indent`t`t<dcsset:selection/>" } + if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" } + else { X "$indent`t`t<dcsset:filter/>" } + if ($ca.appearance) { + X "$indent`t`t<dcsset:appearance>" + foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" } + X "$indent`t`t</dcsset:appearance>" + } + if ($ca.presentation) { + if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) { + # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) + X "$indent`t`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">" + Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t" + X "$indent`t`t</dcsset:presentation>" + } + else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" } + } + if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" } + if ($ca.userSettingID) { + $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } + X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } + if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) { + $useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader') + $set = @{} + foreach ($n in $ca.useInDontUse) { $set["$n"] = $true } + foreach ($n in $useInOrder) { + if ($set.ContainsKey($n)) { + $tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1)) + X "$indent`t`t<dcsset:$tag>DontUse</dcsset:$tag>" + } + } + } + X "$indent`t</dcsset:item>" + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + if ($null -ne $blockUserSettingID) { + $uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" } + X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" + } + X "$indent</$wrapTag>" +} + +# --- 5. Type emitter --- + +$script:formTypeSynonyms = New-Object System.Collections.Hashtable +$script:formTypeSynonyms["строка"] = "string" +$script:formTypeSynonyms["число"] = "decimal" +$script:formTypeSynonyms["булево"] = "boolean" +$script:formTypeSynonyms["дата"] = "date" +$script:formTypeSynonyms["датавремя"]= "dateTime" +$script:formTypeSynonyms["number"] = "decimal" +$script:formTypeSynonyms["bool"] = "boolean" +$script:formTypeSynonyms["справочникссылка"] = "CatalogRef" +$script:formTypeSynonyms["справочникобъект"] = "CatalogObject" +$script:formTypeSynonyms["документссылка"] = "DocumentRef" +$script:formTypeSynonyms["документобъект"] = "DocumentObject" +$script:formTypeSynonyms["перечислениессылка"] = "EnumRef" +$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" +$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" +$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" +$script:formTypeSynonyms["задачассылка"] = "TaskRef" +$script:formTypeSynonyms["определяемыйтип"] = "DefinedType" +$script:formTypeSynonyms["характеристика"] = "Characteristic" +$script:formTypeSynonyms["любаяссылка"] = "AnyRef" +$script:formTypeSynonyms["любаяссылкаиб"] = "AnyIBRef" +# Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: (эмитим verbatim) +$script:formTypeSynonyms["standardperiod"] = "v8:StandardPeriod" +$script:formTypeSynonyms["стандартныйпериод"] = "v8:StandardPeriod" +$script:formTypeSynonyms["standardbeginningdate"] = "v8:StandardBeginningDate" +$script:formTypeSynonyms["стандартнаядатаначала"] = "v8:StandardBeginningDate" +$script:formTypeSynonyms["uuid"] = "v8:UUID" +$script:formTypeSynonyms["уникальныйидентификатор"] = "v8:UUID" +$script:formTypeSynonyms["списокзначений"] = "ValueList" + +# Known invalid types (runtime/UI types that don't exist in XDTO schema) +$script:knownInvalidTypes = @{ + "FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)" + "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" +} + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) + if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] } + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $base = $Matches[1].Trim(); $params = $Matches[2] + $r = $script:formTypeSynonyms[$base.ToLower()] + if ($r) { return "$r($params)" } + return $typeStr + } + if ($typeStr.Contains('.')) { + $i = $typeStr.IndexOf('.') + $prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i) + $r = $script:formTypeSynonyms[$prefix.ToLower()] + if ($r) { return "$r$suffix" } + return $typeStr + } + $r = $script:formTypeSynonyms[$typeStr.ToLower()] + if ($r) { return $r } + return $typeStr +} + +function Emit-Type { + # $tag/$tagAttrs — обёртка (по умолчанию <Type>); для уточнения типа значений ValueList + # вызывается с tag="Settings", tagAttrs=' xsi:type="v8:TypeDescription"'. + param($typeStr, [string]$indent, [string]$tag = "Type", [string]$tagAttrs = "") + + if (-not $typeStr) { + X "$indent<$tag$tagAttrs/>" + return + } + + $typeString = "$typeStr" + + # Composite type: "Type1 | Type2" or "Type1 + Type2" + $parts = $typeString -split '\s*[|+]\s*' + + X "$indent<$tag$tagAttrs>" + foreach ($part in $parts) { + $part = $part.Trim() + Emit-SingleType -typeStr $part -indent "$indent`t" + } + X "$indent</$tag>" +} + +function Emit-SingleType { + param([string]$typeStr, [string]$indent) + + $typeStr = Resolve-TypeStr $typeStr + + # TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так + # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID + # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. + if ($typeStr -match '^typeid:([0-9a-fA-F-]{36})$') { + X "$indent<v8:TypeId>$($Matches[1])</v8:TypeId>" + return + } + + # boolean + if ($typeStr -eq "boolean") { + X "$indent<v8:Type>xs:boolean</v8:Type>" + return + } + + # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) + if ($typeStr -match '^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' } + X "$indent<v8:Type>xs:string</v8:Type>" + X "$indent<v8:StringQualifiers>" + X "$indent`t<v8:Length>$len</v8:Length>" + X "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>" + X "$indent</v8:StringQualifiers>" + return + } + + # decimal(D,F) or decimal(D,F,nonneg) + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indent<v8:Type>xs:decimal</v8:Type>" + X "$indent<v8:NumberQualifiers>" + X "$indent`t<v8:Digits>$digits</v8:Digits>" + X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>" + X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>" + X "$indent</v8:NumberQualifiers>" + return + } + + # date / dateTime / time + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + "time" { "Time" } + } + X "$indent<v8:Type>xs:dateTime</v8:Type>" + X "$indent<v8:DateQualifiers>" + X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>" + X "$indent</v8:DateQualifiers>" + return + } + + # ValueTable, ValueTree, ValueList, etc. + $v8Types = @{ + "ValueTable" = "v8:ValueTable" + "ValueTree" = "v8:ValueTree" + "ValueList" = "v8:ValueListType" + "TypeDescription" = "v8:TypeDescription" + "Universal" = "v8:Universal" + "FixedArray" = "v8:FixedArray" + "FixedStructure" = "v8:FixedStructure" + } + if ($v8Types.ContainsKey($typeStr)) { + X "$indent<v8:Type>$($v8Types[$typeStr])</v8:Type>" + return + } + + # UI types + $uiTypes = @{ + "FormattedString" = "v8ui:FormattedString" + "Picture" = "v8ui:Picture" + "Color" = "v8ui:Color" + "Font" = "v8ui:Font" + } + if ($uiTypes.ContainsKey($typeStr)) { + X "$indent<v8:Type>$($uiTypes[$typeStr])</v8:Type>" + return + } + + # DCS types + if ($typeStr -match '^DataComposition') { + $dcsMap = @{ + "DataCompositionSettings" = "dcsset:DataCompositionSettings" + "DataCompositionSchema" = "dcssch:DataCompositionSchema" + "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" + } + if ($dcsMap.ContainsKey($typeStr)) { + X "$indent<v8:Type>$($dcsMap[$typeStr])</v8:Type>" + return + } + } + + # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. + # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. (Дотированные формы + # ConstantsSet.X / ReportObject.X ловит общий cfg:-regex ниже.) + if ($typeStr -in @("DynamicList","ConstantsSet","ReportObject")) { + X "$indent<v8:Type>cfg:$typeStr</v8:Type>" + return + } + + # TypeSet (набор типов) → <v8:TypeSet>: определяемый тип / характеристика (именованные) + # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. + if ($typeStr -match '^(DefinedType|Characteristic)\.') { + X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>" + return + } + if ($typeStr -match '^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$') { + X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>" + return + } + + # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) + if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') { + X "$indent<v8:Type>cfg:$typeStr</v8:Type>" + return + } + + # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на <v8:Type>). + # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. + # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, + # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. + $specialTypeNs = @{ + "mxl:SpreadsheetDocument" = "http://v8.1c.ru/8.2/data/spreadsheet" + "fd:FormattedDocument" = "http://v8.1c.ru/8.2/data/formatted-document" + "d5p1:TextDocument" = "http://v8.1c.ru/8.1/data/txtedt" + "d5p1:Chart" = "http://v8.1c.ru/8.2/data/chart" + "d5p1:GanttChart" = "http://v8.1c.ru/8.2/data/chart" + "d5p1:Dendrogram" = "http://v8.1c.ru/8.2/data/chart" + "d5p1:FlowchartContextType" = "http://v8.1c.ru/8.2/data/graphscheme" + "d5p1:DataAnalysisTimeIntervalUnitType" = "http://v8.1c.ru/8.2/data/data-analysis" + "d5p1:GeographicalSchema" = "http://v8.1c.ru/8.2/data/geo" + "pdfdoc:PDFDocument" = "http://v8.1c.ru/8.3/data/pdf" + "pl:Planner" = "http://v8.1c.ru/8.3/data/planner" + } + if ($specialTypeNs.ContainsKey($typeStr)) { + $pref = $typeStr.Substring(0, $typeStr.IndexOf(':')) + X "$indent<v8:Type xmlns:$pref=`"$($specialTypeNs[$typeStr])`">$typeStr</v8:Type>" + return + } + + # Fallback with validation + if ($script:knownInvalidTypes.ContainsKey($typeStr)) { + throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])" + } + # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — эмитим verbatim (напр. v8:UUID, v8:StandardPeriod). + if ($typeStr -match '^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):') { + X "$indent<v8:Type>$typeStr</v8:Type>" + } elseif ($typeStr.Contains('.')) { + X "$indent<v8:Type>cfg:$typeStr</v8:Type>" + } else { + Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix" + X "$indent<v8:Type>$typeStr</v8:Type>" + } +} + +# --- 6. Event handler name generator --- + +$script:eventSuffixMap = @{ + "OnChange" = "ПриИзменении" + "StartChoice" = "НачалоВыбора" + "ChoiceProcessing" = "ОбработкаВыбора" + "AutoComplete" = "АвтоПодбор" + "Clearing" = "Очистка" + "Opening" = "Открытие" + "Click" = "Нажатие" + "OnActivateRow" = "ПриАктивизацииСтроки" + "BeforeAddRow" = "ПередНачаломДобавления" + "BeforeDeleteRow" = "ПередУдалением" + "BeforeRowChange" = "ПередНачаломИзменения" + "OnStartEdit" = "ПриНачалеРедактирования" + "OnEndEdit" = "ПриОкончанииРедактирования" + "Selection" = "ВыборСтроки" + "OnCurrentPageChange" = "ПриСменеСтраницы" + "TextEditEnd" = "ОкончаниеВводаТекста" + "URLProcessing" = "ОбработкаНавигационнойСсылки" + "DragStart" = "НачалоПеретаскивания" + "Drag" = "Перетаскивание" + "DragCheck" = "ПроверкаПеретаскивания" + "Drop" = "Помещение" + "AfterDeleteRow" = "ПослеУдаления" +} + +function Get-HandlerName { + param([string]$elementName, [string]$eventName) + $suffix = $script:eventSuffixMap[$eventName] + if ($suffix) { + return "$elementName$suffix" + } + return "$elementName$eventName" +} + +# --- 7. Element emitters --- + +function Get-ElementName { + param($el, [string]$typeKey) + if ($el.name) { return "$($el.name)" } + return "$($el.$typeKey)" +} + +$script:knownEvents = @{ + "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") + "check" = @("OnChange") + "radio" = @("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" = @() +} +$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") + +# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. +# Основной формат: $el.events = { Событие: ИмяОбработчика } (null/"" → авто-имя по конвенции). +# Legacy (принимается ради совместимости): $el.on (массив) + $el.handlers (переопределение имён). +function Get-EventPairs { + param($el, [string]$elementName) + $pairs = New-Object System.Collections.ArrayList + if ($el.events) { + foreach ($p in $el.events.PSObject.Properties) { + $h = "$($p.Value)" + if ([string]::IsNullOrEmpty($h)) { $h = Get-HandlerName -elementName $elementName -eventName $p.Name } + [void]$pairs.Add([pscustomobject]@{ name = $p.Name; handler = $h }) + } + } elseif ($el.on) { + foreach ($evt in $el.on) { + $evtName = "$evt" + $h = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } else { Get-HandlerName -elementName $elementName -eventName $evtName } + [void]$pairs.Add([pscustomobject]@{ name = $evtName; handler = $h }) + } + } + return $pairs +} + +# Проверить, подключено ли событие к элементу (в любом из форматов). +function Test-ElementEvent { + param($el, [string]$eventName) + if ($el.events) { + foreach ($p in $el.events.PSObject.Properties) { if ($p.Name -eq $eventName) { return $true } } + } + if ($el.on -contains $eventName) { return $true } + return $false +} + +function Emit-Events { + param($el, [string]$elementName, [string]$indent, [string]$typeKey) + + $pairs = Get-EventPairs -el $el -elementName $elementName + if ($pairs.Count -eq 0) { return } + + # Validate event names + if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { + $allowed = $script:knownEvents[$typeKey] + foreach ($pr in $pairs) { + if ($allowed.Count -gt 0 -and $allowed -notcontains "$($pr.name)") { + Write-Host "[WARN] Unknown event '$($pr.name)' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + } + } + } + + X "$indent<Events>" + foreach ($pr in $pairs) { + X "$indent`t<Event name=`"$($pr.name)`">$($pr.handler)</Event>" + } + X "$indent</Events>" +} + +# ExtendedTooltip — это LabelDecoration: может нести own-content (layout/оформление/флаги/hyperlink) +# вместо/вместе с текстом. Признак структурированной формы: объект с любым НЕ-текстовым ключом +# ({text,formatted} и языковые ключи {ru,en} → обычная текст-форма). +$script:companionStructKeys = @( + 'width','autoMaxWidth','maxWidth','height','autoMaxHeight','maxHeight','verticalAlign','titleHeight', + 'horizontalStretch','verticalStretch','horizontalAlign','groupHorizontalAlign','groupVerticalAlign', + 'visible','hidden','enabled','disabled','hyperlink','events', + 'textColor','backColor','borderColor','font','border','цветтекста','цветфона','цветрамки','шрифт','рамка' +) +function Test-CompanionStructured { + param($content) + if (-not (($content -is [System.Collections.IDictionary]) -or ($content -is [System.Management.Automation.PSCustomObject]))) { return $false } + foreach ($k in $script:companionStructKeys) { + $present = if ($content -is [System.Collections.IDictionary]) { $content.Contains($k) } else { [bool]$content.PSObject.Properties[$k] } + if ($present) { return $true } + } + return $false +} + +function Emit-CompanionTitle { + param($content, [string]$indent) + $r = Resolve-MLFormatted $content + $fmt = if ($r.formatted) { 'true' } else { 'false' } + X "$indent<Title formatted=`"$fmt`">" + Emit-MLItems -val $r.text -indent "$indent`t" + X "$indent" +} + +# DisplayImportance — атрибут открывающего тега элемента (адаптивная важность: VeryHigh/High/Usual/Low/VeryLow). +# Возвращает ` DisplayImportance="X"` или "" (для companion-эмиттеров без $el → "" молча). +function DI-Attr { + param($el) + if ($null -ne $el -and $el.displayImportance) { return " DisplayImportance=`"$(Esc-Xml "$($el.displayImportance)")`"" } + return "" +} + +function Emit-Companion { + param([string]$tag, [string]$name, [string]$indent, $content = $null) + $id = New-Id + $hasContent = $null -ne $content -and -not ($content -is [string] -and "$content" -eq '') + if (-not $hasContent) { + X "$indent<$tag name=`"$name`" id=`"$id`"/>" + return + } + $inner = "$indent`t" + X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" + if (Test-CompanionStructured $content) { + # структурированная форма (own-content). Порядок как у платформы: own-content (флаги/hyperlink/ + # layout/оформление) ПЕРЕД Title (в корпусе layout-first 582 vs 10). + $txtPresent = if ($content -is [System.Collections.IDictionary]) { $content.Contains('text') } else { [bool]$content.PSObject.Properties['text'] } + Emit-CommonFlags -el $content -indent $inner + if ($content.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $content -indent $inner + Emit-Appearance -el $content -indent $inner -profile 'decoration' + if ($txtPresent) { Emit-CompanionTitle -content $content -indent $inner } + # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) + Emit-Events -el $content -elementName $name -indent $inner -typeKey 'label' + } else { + Emit-CompanionTitle -content $content -indent $inner + } + X "$indent" +} + +# Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, children?[] } +# или массив = shorthand для { children }. Пусто/нет → self-closing companion (как Emit-Companion). +# Дети — обычная грамматика button/buttonGroup/popup (Emit-Element, inCmdBar). +function Emit-CompanionPanel { + param([string]$tag, [string]$name, [string]$indent, $panel) + $id = New-Id + $autofill = $null + $children = $null + $halign = $null + if ($panel -is [array]) { + $children = $panel + } elseif ($null -ne $panel) { + if ($null -ne $panel.PSObject.Properties['autofill'] -and $null -ne $panel.autofill) { $autofill = [bool]$panel.autofill } + if ($null -ne $panel.PSObject.Properties['horizontalAlign'] -and "$($panel.horizontalAlign)" -ne '') { $halign = "$($panel.horizontalAlign)" } + $children = $panel.children + } + $hasChildren = $children -and @($children).Count -gt 0 + # Платформа пишет только при false; true = дефолт (тег опускается). + $emitAfFalse = ($autofill -eq $false) + if (-not $emitAfFalse -and -not $hasChildren -and -not $halign) { + X "$indent<$tag name=`"$name`" id=`"$id`"/>" + return + } + X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" + if ($halign) { X "$indent`t$halign" } + if ($emitAfFalse) { X "$indent`tfalse" } + if ($hasChildren) { + X "$indent`t" + foreach ($c in @($children)) { Emit-Element -el $c -indent "$indent`t`t" -inCmdBar $true } + X "$indent`t" + } + X "$indent" +} + +# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type. +$script:additionTypeMap = [ordered]@{ + 'searchString' = @{ Tag = 'SearchStringAddition'; Type = 'SearchStringRepresentation'; Suffix = 'СтрокаПоиска' } + 'viewStatus' = @{ Tag = 'ViewStatusAddition'; Type = 'ViewStatusRepresentation'; Suffix = 'СостояниеПросмотра' } + 'searchControl' = @{ Tag = 'SearchControlAddition'; Type = 'SearchControl'; Suffix = 'УправлениеПоиском' } +} +# Синонимы типа дополнения (для override-карты additions и резолва тип-ключа). +$script:additionKeySynonyms = @{ + 'searchString' = @('SearchStringAddition','SearchStringRepresentation','строкаПоиска','отображениеСтрокиПоиска') + 'viewStatus' = @('ViewStatusAddition','ViewStatusRepresentation','состояниеПросмотра') + 'searchControl' = @('SearchControlAddition','SearchControl','управлениеПоиском') +} + +# HorizontalLocation: auto (дефолт, тег опускаем) / left / right; forgiving + рус.синонимы. +function Get-HLocation { + param($el) + $v = if ($el -and $el.PSObject.Properties['horizontalLocation']) { $el.horizontalLocation } else { $null } + if (-not $v) { return $null } + switch -Regex ("$v".ToLower()) { + '^(auto|авто)$' { return $null } # дефолт — не эмитим + '^(left|слева|лево)$' { return 'Left' } + '^(right|справа|право)$' { return 'Right' } + '^(center|центр|по центру)$' { return 'Center' } + default { return "$v" } + } +} + +# Тело дополнения: AdditionSource + свойства (как у поля) + companions. $props может быть $null +# (стандартное дополнение без отклонений). Порядок 1С-толерантен (diff порядок-независим). +function Emit-AdditionBody { + param($props, [string]$source, [string]$srcType, [string]$addName, [string]$indent) + $inner = "$indent`t" + X "$inner" + X "$inner`t$source" + X "$inner`t$srcType" + X "$inner" + if ($props) { + if ($props.PSObject.Properties['title'] -and $props.title) { Emit-MLText -tag "Title" -text $props.title -indent $inner } + Emit-CommonFlags -el $props -indent $inner + if ($props.tooltip) { Emit-MLText -tag "ToolTip" -text $props.tooltip -indent $inner } + if ($props.tooltipRepresentation) { X "$inner$($props.tooltipRepresentation)" } + $hl = Get-HLocation $props; if ($hl) { X "$inner$hl" } + Emit-Layout -el $props -indent $inner + Emit-Appearance -el $props -indent $inner -profile 'field' + } + Emit-Companion -tag "ContextMenu" -name "${addName}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${addName}РасширеннаяПодсказка" -indent $inner +} + +# Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. +function Emit-Addition { + param($el, [string]$name, [int]$id, [string]$typeKey, [string]$indent) + $map = $script:additionTypeMap[$typeKey] + $source = if ($el.source) { "$($el.source)" } elseif ($script:currentTableName) { $script:currentTableName } else { '' } + X "$indent<$($map.Tag) name=`"$name`" id=`"$id`"$(DI-Attr $el)>" + Emit-AdditionBody -props $el -source $source -srcType $map.Type -addName $name -indent $indent + X "$indent" +} + +# Стандартное табличное дополнение (авто-генерация на уровне таблицы). $override — объект отклонений +# из per-table карты additions (или $null = чистый дефолт). +function Emit-TableAddition { + param([string]$typeKey, [string]$tableName, [string]$indent, $override = $null) + $map = $script:additionTypeMap[$typeKey] + $addName = "$tableName$($map.Suffix)" + $id = New-Id + X "$indent<$($map.Tag) name=`"$addName`" id=`"$id`">" + Emit-AdditionBody -props $override -source $tableName -srcType $map.Type -addName $addName -indent $indent + X "$indent" +} + +# Прочитать override-объект для типа дополнения из per-table карты additions (с синонимами). +function Get-AdditionOverride { + param($additions, [string]$typeKey) + if ($null -eq $additions) { return $null } + foreach ($k in @($typeKey) + $script:additionKeySynonyms[$typeKey]) { + $p = $additions.PSObject.Properties[$k] + if ($p) { return $p.Value } + } + return $null +} + +function Emit-Element { + param($el, [string]$indent, [bool]$inCmdBar = $false) + + # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. + Normalize-PanelSynonyms $el + + # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). + # Maps any synonym to canonical short DSL key. + # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя); + # объект/массив уже отнесён к панель-свойству выше. + $strOnlyKeys = @('commandBar','autoCommandBar','КоманднаяПанель') + $synonyms = @{ + "commandBar" = "cmdBar" + "autoCommandBar" = "autoCmdBar" + "КоманднаяПанель" = "cmdBar" + "InputField" = "input" + "ПолеВвода" = "input" + "CheckBoxField" = "check" + "ПолеФлажка" = "check" + "RadioButtonField" = "radio" + "ПолеПереключателя" = "radio" + "radioButton" = "radio" + "PictureField" = "picField" + "ПолеКартинки" = "picField" + "LabelField" = "labelField" + "ПолеНадписи" = "labelField" + "CalendarField" = "calendar" + "ПолеКалендаря" = "calendar" + "LabelDecoration" = "label" + "Надпись" = "label" + "PictureDecoration" = "picture" + "Картинка" = "picture" + "UsualGroup" = "group" + "Группа" = "group" + "ОбычнаяГруппа" = "group" + "ColumnGroup" = "columnGroup" + "ГруппаКолонок" = "columnGroup" + "Pages" = "pages" + "ГруппаСтраниц" = "pages" + "Page" = "page" + "Страница" = "page" + "Table" = "table" + "Таблица" = "table" + "Button" = "button" + "Кнопка" = "button" + "Popup" = "popup" + "ВсплывающееМеню" = "popup" + # Дополнения командной панели таблицы (тип-как-ключ) — forgiving: XML-тег/Type/рус.имя → канон + "SearchStringAddition" = "searchString" + "SearchStringRepresentation" = "searchString" + "строкаПоиска" = "searchString" + "отображениеСтрокиПоиска" = "searchString" + "Отображение строки поиска" = "searchString" + "ViewStatusAddition" = "viewStatus" + "ViewStatusRepresentation" = "viewStatus" + "состояниеПросмотра" = "viewStatus" + "Состояние просмотра" = "viewStatus" + "SearchControlAddition" = "searchControl" + "SearchControl" = "searchControl" + "управлениеПоиском" = "searchControl" + "Управление поиском" = "searchControl" + # Спец-поля (документ/датчик) — XML-имя/рус. → канон + "SpreadSheetDocumentField" = "spreadsheet" + "ПолеТабличногоДокумента" = "spreadsheet" + "HTMLDocumentField" = "html" + "ПолеHTMLДокумента" = "html" + "TextDocumentField" = "textDoc" + "ПолеТекстовогоДокумента" = "textDoc" + "FormattedDocumentField" = "formattedDoc" + "ПолеФорматированногоДокумента" = "formattedDoc" + "ProgressBarField" = "progressBar" + "ПолеИндикатора" = "progressBar" + "TrackBarField" = "trackBar" + "ПолеПолосыРегулирования" = "trackBar" + "ChartField" = "chart" + "ПолеДиаграммы" = "chart" + "GanttChartField" = "ganttChart" + "ПолеДиаграммыГанта" = "ganttChart" + "GraphicalSchemaField" = "graphicalSchema" + "ПолеГрафическойСхемы" = "graphicalSchema" + "PlannerField" = "planner" + "ПолеПланировщика" = "planner" + "PeriodField" = "periodField" + "ПолеПериода" = "periodField" + "DendrogramField" = "dendrogram" + "ПолеДендрограммы" = "dendrogram" + } + foreach ($pair in $synonyms.GetEnumerator()) { + if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) { + if ($strOnlyKeys -contains $pair.Key -and -not ($el.($pair.Key) -is [string])) { continue } + $val = $el.($pair.Key) + $el.PSObject.Properties.Remove($pair.Key) | Out-Null + $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force + } + } + + # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. + # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. + foreach ($pn in @($el.PSObject.Properties.Name)) { + $norm = ($pn -replace '\s','').ToLower() + $canon = $script:propSynonyms[$norm] + if ($canon -and $pn -ne $canon) { + if ($null -eq $el.PSObject.Properties[$canon]) { + $val = $el.($pn) + $el | Add-Member -NotePropertyName $canon -NotePropertyValue $val -Force + } + $el.PSObject.Properties.Remove($pn) | Out-Null + } + } + + # Determine element type from key + $typeKey = $null + $xmlTag = $null + + # picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка + # у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. + # pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей + # (Horizontal), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. + foreach ($key in @("columnGroup","buttonGroup","pages","page","group","input","check","radio","label","labelField","table","button","calendar","cmdBar","popup","searchString","viewStatus","searchControl","picField","picture","spreadsheet","html","textDoc","formattedDoc","progressBar","trackBar","chart","ganttChart","graphicalSchema","planner","periodField","dendrogram")) { + if ($el.$key -ne $null) { + $typeKey = $key + break + } + } + + if (-not $typeKey) { + Write-Warning "Unknown element type, skipping" + return + } + + # Validate known keys — warn about typos and unknown properties + $knownKeys = @{ + # type keys + "group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры + "spreadsheet"=1;"html"=1;"textDoc"=1;"formattedDoc"=1;"progressBar"=1;"trackBar"=1 + "chart"=1;"ganttChart"=1;"graphicalSchema"=1;"planner"=1;"periodField"=1;"dendrogram"=1;"ganttTable"=1 + "showPercent"=1;"largeStep"=1;"markingStep"=1;"step"=1 + "horizontalScrollBar"=1;"viewScalingMode"=1;"output"=1;"selectionShowMode"=1;"protection"=1 + "edit"=1;"showGrid"=1;"showGroups"=1;"showHeaders"=1;"showRowAndColumnNames"=1;"showCellNames"=1 + "pointerType"=1;"drawingSelectionShowMode"=1;"warningOnEditRepresentation"=1;"markingAppearance"=1 + # report-form контекст (generic-скаляры элементов) + "horizontalSpacing"=1;"representationInContextMenu"=1;"settingsNamedItemDetailedRepresentation"=1 + # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель + "itemHeight"=1;"dropListWidth"=1;"choiceButtonPicture"=1;"transparentPixel"=1 + # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы + "titleDataPath"=1;"extendedEdit"=1;"maxRowsCount"=1;"autoMaxRowsCount"=1;"heightControlVariant"=1 + "warningOnEdit"=1;"nonselectedPictureText"=1;"editTextUpdate"=1;"footerText"=1 + # columnGroup-specific + "showInHeader"=1 + # radio-specific + "radioButtonType"=1;"choiceList"=1;"columnsCount"=1;"checkBoxType"=1;"editMode"=1 + # naming & binding + "name"=1;"path"=1;"title"=1;"tooltip"=1;"tooltipRepresentation"=1;"extendedTooltip"=1 + # companion-панели (свойства): командная панель + контекстное меню + "commandBar"=1;"contextMenu"=1 + # источник команд группы/панели (ButtonGroup/CommandBar) + "commandSource"=1 + # visibility & state + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 + # events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости) + "events"=1;"on"=1;"handlers"=1 + # layout + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + "maxWidth"=1;"maxHeight"=1 + "groupHorizontalAlign"=1;"groupVerticalAlign"=1;"horizontalAlign"=1 + # input-specific + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + "textEdit"=1 + "wrap"=1;"openButton"=1;"listChoiceMode"=1;"showInFooter"=1 + "extendedEditMultipleValues"=1;"chooseType"=1;"autoCellHeight"=1 + "choiceButtonRepresentation"=1;"footerHorizontalAlign"=1;"headerHorizontalAlign"=1 + "format"=1;"editFormat"=1;"choiceParameters"=1;"choiceParameterLinks"=1;"typeLink"=1 + # label/hyperlink + "hyperlink"=1;"formatted"=1 + # group-specific + "collapsedTitle"=1;"showTitle"=1;"united"=1;"collapsed"=1;"behavior"=1 + # hierarchy + "children"=1;"columns"=1 + # table-specific + "changeRowSet"=1;"changeRowOrder"=1;"autoInsertNewRow"=1;"rowFilter"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1;"viewStatusLocation"=1;"searchControlLocation"=1 + "excludedCommands"=1 + "choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1 + "rowPictureDataPath"=1;"tableAutofill"=1;"heightInTableRows"=1 + "multipleChoice"=1;"searchOnInput"=1;"shortcut"=1 + "rowSelectionMode"=1;"verticalLines"=1;"horizontalLines"=1 + # dynamic-list table block + "defaultItem"=1;"useAlternationRowColor"=1;"fileDragMode"=1;"autoRefresh"=1 + "autoRefreshPeriod"=1;"choiceFoldersAndItems"=1;"restoreCurrentRow"=1;"showRoot"=1 + "allowRootChoice"=1;"updateOnDataChange"=1;"allowGettingCurrentRowURL"=1 + "userSettingsGroup"=1;"rowsPicture"=1 + # calendar-specific + "selectionMode"=1;"showCurrentDate"=1;"widthInMonths"=1;"heightInMonths"=1;"showMonthsPanel"=1 + # pages-specific + "pagesRepresentation"=1 + # button-specific + "type"=1;"command"=1;"commandName"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1;"displayImportance"=1 + # picture/decoration + "src"=1;"valuesPicture"=1;"loadTransparent"=1;"headerPicture"=1;"footerPicture"=1 + # cmdBar-specific + "autofill"=1 + # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице + "autoCmdBar"=1 + # дополнения командной панели таблицы (тип-ключи + свойства) + "searchString"=1;"viewStatus"=1;"searchControl"=1;"source"=1;"horizontalLocation"=1;"additions"=1 + # generic-скаляры (pass-through) + точечные + "verticalAlign"=1;"throughAlign"=1;"enableContentChange"=1;"pictureSize"=1;"titleHeight"=1 + "childItemsWidth"=1;"showLeftMargin"=1;"cellHyperlink"=1;"viewMode"=1;"verticalScrollBar"=1 + "rowInputMode"=1;"mask"=1;"createButton"=1;"fixingInTable"=1;"verticalSpacing"=1 + # InputField choice-скаляры + "choiceListButton"=1;"quickChoice"=1;"autoChoiceIncomplete"=1 + "choiceForm"=1;"choiceHistoryOnInput"=1;"footerDataPath"=1;"minValue"=1;"maxValue"=1 + # Button — пометка toggle-кнопки (ключ 'checked', не 'check' — во избежание конфликта с типом) + "checked"=1 + } + # Оформление (цвета/шрифты/граница) — авто-регистрация из самих структур, чтобы allowlist + # не дрейфовал при добавлении новых ключей/синонимов. Канонические + forgiving-синонимы. + foreach ($k in $script:appearanceSpec.Keys) { $knownKeys[$k] = 1 } + foreach ($k in $script:appearanceSynonyms.Keys) { $knownKeys[$k] = 1 } + foreach ($k in $script:propSynonyms.Keys) { $knownKeys[$k] = 1 } + foreach ($p in $el.PSObject.Properties) { + if ($p.Name -like '_*') { continue } # внутренние маркеры (напр. _dynList) + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." + } + } + + $name = Get-ElementName -el $el -typeKey $typeKey + Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element' + $id = New-Id + + switch ($typeKey) { + "group" { Emit-Group -el $el -name $name -id $id -indent $indent } + "columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent } + "buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent } + "input" { Emit-Input -el $el -name $name -id $id -indent $indent } + "check" { Emit-Check -el $el -name $name -id $id -indent $indent } + "radio" { Emit-Radio -el $el -name $name -id $id -indent $indent } + "label" { Emit-Label -el $el -name $name -id $id -indent $indent } + "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } + "table" { Emit-Table -el $el -name $name -id $id -indent $indent } + "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } + "page" { Emit-Page -el $el -name $name -id $id -indent $indent } + "button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar } + "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } + "searchString" { Emit-Addition -el $el -name $name -typeKey "searchString" -id $id -indent $indent } + "viewStatus" { Emit-Addition -el $el -name $name -typeKey "viewStatus" -id $id -indent $indent } + "searchControl" { Emit-Addition -el $el -name $name -typeKey "searchControl" -id $id -indent $indent } + "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } + "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } + "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } + "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } + "spreadsheet" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "SpreadSheetDocumentField" -typeKey "spreadsheet" } + "html" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "HTMLDocumentField" -typeKey "html" } + "textDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TextDocumentField" -typeKey "textDoc" } + "formattedDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "FormattedDocumentField" -typeKey "formattedDoc" } + "progressBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ProgressBarField" -typeKey "progressBar" } + "trackBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TrackBarField" -typeKey "trackBar" } + "chart" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ChartField" -typeKey "chart" } + "graphicalSchema" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "GraphicalSchemaField" -typeKey "graphicalSchema" } + "planner" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PlannerField" -typeKey "planner" } + "periodField" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PeriodField" -typeKey "periodField" } + "dendrogram" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "DendrogramField" -typeKey "dendrogram" } + "ganttChart" { Emit-GanttChart -el $el -name $name -id $id -indent $indent } + } +} + +# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). +# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). +# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. +# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. +function Emit-XrFlag { + param([string]$tag, $val, [string]$indent) + if ($null -eq $val) { return } + if ($val -is [bool]) { + X "$indent<$tag>" + X "$indent`t$(if ($val){'true'}else{'false'})" + X "$indent" + return + } + # объектная форма { common, roles } + $common = if ($null -ne $val.common) { [bool]$val.common } else { $false } + X "$indent<$tag>" + X "$indent`t$(if ($common){'true'}else{'false'})" + if ($val.roles) { + foreach ($r in $val.roles.PSObject.Properties) { + # Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". + # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. + $rname = "$($r.Name)" -replace '^(Role|Роль)\.', '' + if ($rname -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $rname = "Role.$rname" } + $rval = if ([bool]$r.Value) { 'true' } else { 'false' } + X "$indent`t$rval" + } + } + X "$indent" +} + +function Emit-CommonFlags { + param($el, [string]$indent) + if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } + if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent } + if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } + if ($el.readOnly -eq $true) { X "$indenttrue" } +} + +# Общие layout-свойства — применимы ко всем элементам. Порядок согласован с +# историческим выводом input/label, чтобы не сдвигать существующие снапшоты. +# -skipHeight: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). +# -multiLineDefault: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. +# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. +function Emit-CommonElementProps { + param($el, [string]$indent) + if ($el.defaultItem -eq $true) { X "$indenttrue" } + if ($el.PSObject.Properties['skipOnInput'] -and $null -ne $el.skipOnInput) { + $siv = if ($el.skipOnInput -eq $true) { 'true' } else { 'false' } + X "$indent$siv" + } + # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) + if ($null -ne $el.enableStartDrag) { X "$indent$(if ($el.enableStartDrag){'true'}else{'false'})" } + if ($el.fileDragMode) { X "$indent$($el.fileDragMode)" } + # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» + foreach ($p in @(@('showInHeader','ShowInHeader'), @('showInFooter','ShowInFooter'), @('autoCellHeight','AutoCellHeight'))) { + if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } + } + if ($el.footerHorizontalAlign) { X "$indent$($el.footerHorizontalAlign)" } + if ($el.headerHorizontalAlign) { X "$indent$($el.headerHorizontalAlign)" } +} + +# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). +# Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). +# Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. +# src с префиксом "abs:" → встроенная картинка ; иначе именованная/стилевая . +function Emit-PictureRef { + param($val, [string]$picTag, [string]$indent) + if (-not $val) { return } + $src = $null; $lt = $false; $tpx = $null + if ($val -is [string]) { $src = $val } + else { $src = $val.src; if ($val.loadTransparent -eq $true) { $lt = $true }; $tpx = $val.transparentPixel } + if (-not $src) { return } + $srcStr = "$src" + X "$indent<$picTag>" + if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } + else { X "$indent`t$(Esc-Xml $srcStr)" } + X "$indent`t$(if ($lt) { 'true' } else { 'false' })" + if ($tpx) { X "$indent`t" } + X "$indent" +} + +# Картинки заголовка/подвала колонки поля — по схеме сразу после , +# перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь). +function Emit-ColumnPics { + param($el, [string]$indent) + Emit-PictureRef -val $el.headerPicture -picTag 'HeaderPicture' -indent $indent + Emit-PictureRef -val $el.footerPicture -picTag 'FooterPicture' -indent $indent +} + +# кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false +# (обратная конвенция относительно header/values-картинок). Прощающий ввод: +# принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель +# опишет картинку объектно по аналогии с headerPicture. $elemLt — legacy +# элемент-уровневый ключ loadTransparent (используется, если в объекте флаг не задан). +function Emit-CommandPicture { + param($pic, $elemLt, [string]$indent) + if (-not $pic) { return } + $src = $null; $lt = $null; $tpx = $null + if ($pic -is [string]) { $src = $pic } + else { $src = $pic.src; if ($null -ne $pic.loadTransparent) { $lt = [bool]$pic.loadTransparent }; $tpx = $pic.transparentPixel } + if (-not $src) { return } + if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt } + $srcStr = "$src" + X "$indent" + if ($srcStr -match '^abs:(.*)$') { X "$indent`t$(Esc-Xml $matches[1])" } + else { X "$indent`t$(Esc-Xml $srcStr)" } + X "$indent`t$(if ($lt -eq $false) { 'false' } else { 'true' })" + if ($tpx) { X "$indent`t" } + X "$indent" +} + +# --- Оформление элемента: цвета / шрифты / граница --- +# Прямые свойства элемента (// + header/footer-варианты у полей). +# Ключи DSL — англ. camelCase 1:1 с тегами; принимаем рус. синонимы (forgiving). +# Значения: цвет — verbatim-строка (style:/web:/sys:/win:/#RRGGBB); шрифт — строка-ref или +# объект-атрибуты; граница — строка-ref или объект {width,style|ref}. Порядок тегов — XSD (профиль). +$script:appearanceSpec = @{ + titleTextColor = @{ tag='TitleTextColor'; kind='color' } + titleBackColor = @{ tag='TitleBackColor'; kind='color' } + titleFont = @{ tag='TitleFont'; kind='font' } + footerTextColor = @{ tag='FooterTextColor'; kind='color' } + footerBackColor = @{ tag='FooterBackColor'; kind='color' } + footerFont = @{ tag='FooterFont'; kind='font' } + textColor = @{ tag='TextColor'; kind='color' } + backColor = @{ tag='BackColor'; kind='color' } + borderColor = @{ tag='BorderColor'; kind='color' } + border = @{ tag='Border'; kind='border'} + font = @{ tag='Font'; kind='font' } +} +# Рус. синоним (lower) → canonical key +$script:appearanceSynonyms = @{ + 'цветтекста'='textColor'; 'цветфона'='backColor'; 'цветрамки'='borderColor' + 'цветтекстазаголовка'='titleTextColor'; 'цветфоназаголовка'='titleBackColor'; 'шрифтзаголовка'='titleFont' + 'цветтекстаподвала'='footerTextColor'; 'цветфонаподвала'='footerBackColor'; 'шрифтподвала'='footerFont' + 'шрифт'='font'; 'рамка'='border' +} +# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. +# Ключи карты нормализованы (lowercase, без пробелов); сопоставление в Normalize-PropSynonyms тоже. +# Прощающий ввод: модель может писать свойство по-русски. Англ. ключ работает всегда (это доп. слой). +# Видимость/Доступность НЕ включаем — наш hidden/disabled инвертирован, был бы баг семантики. +$script:propSynonyms = @{ + 'пометка'='checked' + 'кнопкавыбора'='choiceButton'; 'кнопкаочистки'='clearButton'; 'кнопкарегулирования'='spinButton' + 'кнопкавыпадающегосписка'='dropListButton'; 'кнопкасписковоговыбора'='choiceListButton' + 'кнопкаоткрытия'='openButton'; 'кнопкапоумолчанию'='defaultButton' + 'быстрыйвыбор'='quickChoice'; 'формавыбора'='choiceForm'; 'историявыборапривводе'='choiceHistoryOnInput' + 'выборгруппиэлементов'='choiceFoldersAndItems'; 'фиксациявтаблице'='fixingInTable' + 'путькданнымподвала'='footerDataPath'; 'автоотметканезаполненного'='markIncomplete' + 'многострочныйрежим'='multiLine'; 'режимпароля'='passwordMode'; 'переноспословам'='wrap' + 'расположениезаголовка'='titleLocation'; 'пропускатьпривводе'='skipOnInput' + 'заголовок'='title'; 'ширина'='width'; 'высота'='height'; 'подсказкаввода'='inputHint' +} +# Профили порядка тегов по базовым типам (XSD-последовательность) +$script:appOrderField = @('titleTextColor','titleBackColor','titleFont','footerTextColor','footerBackColor','footerFont','textColor','backColor','borderColor','border','font') +$script:appOrderDecoration = @('textColor','font','backColor','borderColor','border') +$script:appOrderButton = @('textColor','backColor','borderColor','font') + +# Простые скаляры элемента (pass-through: captured/emitted «как есть»). Только НЕ-перекрывающиеся +# теги (не обрабатываемые специфично где-то ещё). kind: bool → true/false; value → строка verbatim. +$script:genericScalars = @( + @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } + @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } + @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } + @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } + @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } + @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } + @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } + @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } + @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } + @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } + @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } + @{ Tag='Mask'; Key='mask'; Kind='value' } + @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } + @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } + @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } + # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through + @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } + @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } + @{ Tag='Output'; Key='output'; Kind='value' } + @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } + @{ Tag='PointerType'; Key='pointerType'; Kind='value' } + @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } + @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } + @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } + @{ Tag='Protection'; Key='protection'; Kind='bool' } + @{ Tag='Edit'; Key='edit'; Kind='bool' } + @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } + @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } + @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } + @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } + @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } + @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } + # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы + @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } + @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } + @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } + # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) + @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } + @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } + # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам + @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } + @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } + @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } + @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } + @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } + @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } + # Корпусный хвост: представление управления свёрткой группы / форма кнопки-попапа / + # авто-добавление незаполненной строки / выделение отрицательных / нач. позиция списка / + # высота списка выбора / три состояния флажка / прокрутка страницы при сжатии + @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } + @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } + @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } + @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } + @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } + @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } + @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } + @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } + # Сочетание клавиш — общее свойство (input/group/radio/page/picField/label/table/check; команда — отд. путь, §7) + @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } + # Батч простых скаляров (input/radio/group/picDecoration/button): режим выбора незаполненного, + # равная ширина колонок, выравнивание детей, масштаб/зум картинки, форма/положение картинки кнопки. + # (Table HeaderHeight/FooterHeight/CurrentRowUse — НЕ здесь, а в Emit-Table: pass-through, + # 1С толерантна к порядку детей Table — в корпусе те же теги встречаются в разных позициях.) + @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } + @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } + @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } + @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } + @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } + @{ Tag='Shape'; Key='shape'; Kind='value' } + @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } + # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) + @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } + @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } +) + +function Emit-GenericScalars { + param($el, [string]$indent) + if ($null -eq $el) { return } + foreach ($s in $script:genericScalars) { + $p = $el.PSObject.Properties[$s.Key] + if (-not $p -or $null -eq $p.Value) { continue } + if ($s.Kind -eq 'bool') { + X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})" + } else { + $v = "$($p.Value)"; if ($v -eq '') { continue } + X "$indent<$($s.Tag)>$(Esc-Xml $v)" + } + } +} + +function Get-AppearanceValue { + param($el, [string]$canonical) + if ($null -eq $el) { return $null } + $p = $el.PSObject.Properties[$canonical] + if ($p) { return $p.Value } + foreach ($syn in $script:appearanceSynonyms.Keys) { + if ($script:appearanceSynonyms[$syn] -eq $canonical) { + $pp = $el.PSObject.Properties[$syn] + if ($pp) { return $pp.Value } + } + } + return $null +} + +# — строка = ref на стиль (kind=StyleItem); объект = атрибуты. +function Emit-FontTag { + param([string]$tag, $val, [string]$indent) + if ($val -is [string]) { + X "$indent<$tag ref=`"$(Esc-Xml $val)`" kind=`"StyleItem`"/>" + return + } + $attrs = @() + foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $pp = $val.PSObject.Properties[$a] + if ($pp -and $null -ne $pp.Value) { + $v = $pp.Value + if ($v -is [bool]) { $v = if ($v) {'true'} else {'false'} } + $attrs += "$a=`"$(Esc-Xml "$v")`"" + } + } + X "$indent<$tag $($attrs -join ' ')/>" +} + +# — строка/{ref} = из стиля (); {width,style} = явная. +function Emit-BorderTag { + param($val, [string]$indent) + if ($val -is [string]) { X "$indent"; return } + $refP = $val.PSObject.Properties['ref'] + if ($refP -and $refP.Value) { X "$indent"; return } + $width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 } + $style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null } + X "$indent" + if ($style) { X "$indent`t$(Esc-Xml $style)" } + X "$indent" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time — встроенный конфиг планировщика +# на реквизите planner-типа. Структурный DSL: items[] + appearance/поведение-скаляры + +# timeScale (уровни шкалы времени) + period. Каждое присутствующее поле → каноничный +# порядок; пропущенное → дефолт (планировщик всегда несёт полный блок). Декомпилятор +# делает полный захват → раундтрип бит-в-бит; ручной авторинг может быть кратким. +$script:PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' +$script:CHART_NS = 'http://v8.1c.ru/8.2/data/chart' + +function PL-Get { + param($o, [string]$k, $def = $null) + if ($null -ne $o -and $o.PSObject.Properties[$k] -and $null -ne $o.$k) { return $o.$k } + return $def +} +function PL-Bool { + param($v) + if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } } + elseif ("$v" -eq 'True') { 'true' } + elseif ("$v" -eq 'False') { 'false' } + else { "$v" } +} +function Emit-PlannerColor { + param([string]$tag, $o, [string]$key, [string]$ind) + X "$ind$(Esc-Xml "$(PL-Get $o $key 'auto')")" +} +# /… — пустое → самозакрывающийся тег (как в выгрузке платформы). +function Emit-PlannerText { + param([string]$tag, $v, [string]$ind) + if ([string]::IsNullOrEmpty("$v")) { X "$ind" } + else { X "$ind$(Esc-Xml "$v")" } +} +# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef"; +# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы. +function Test-PlannerRef { + param([string]$v) + return ($v -match '^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' -or ` + $v -match '\.EnumValue\.' -or $v -match 'EmptyRef$' -or ` + $v -match '^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') +} +# — nil (нет значения) / xr:DesignTimeRef (ссылка) / xs:string (строка/прочее). +function Emit-PlannerValue { + param($v, [string]$ind) + if ($null -eq $v -or "$v" -eq '') { X "$ind"; return } + $t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' } + X "$ind$(Esc-Xml "$v")" +} +function Emit-PlannerFont { + param($o, [string]$ind) + $f = PL-Get $o 'font' $null + if ($null -eq $f) { X "$ind"; return } + Emit-FontTag -tag 'pl:font' -val $f -indent $ind +} +function Emit-PlannerBorder { + param($o, [string]$ind, [string]$key = 'border') + $b = PL-Get $o $key $null + $bw = if ($b) { PL-Get $b 'width' 1 } else { 1 } + $bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' } + X "$ind" + X "$ind`t$(Esc-Xml "$bs")" + X "$ind" +} +function Emit-PlannerLevel { + param($lv, [string]$cns, [string]$ind) + $li = "$ind`t" + X "$ind" + X "$li$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")" + X "$li$(PL-Get $lv 'interval' 1)" + X "$li$(PL-Bool (PL-Get $lv 'show' $true))" + $line = PL-Get $lv 'line' $null + $lw = if ($line) { PL-Get $line 'width' 1 } else { 1 } + $lg = if ($line) { PL-Get $line 'gap' $false } else { $false } + $lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' } + X "$li" + X "$li`t$(Esc-Xml "$lst")" + X "$li" + X "$li$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")" + X "$li$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")" + $fmt = PL-Get $lv 'format' $null + if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } } + X "$li" + Emit-MLItems -val $fmt -indent "$li`t" + X "$li" + $labels = PL-Get $lv 'labels' $null + $ticks = if ($labels) { PL-Get $labels 'ticks' 0 } else { 0 } + X "$li" + X "$li`t$ticks" + X "$li" + X "$li$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")" + X "$li$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")" + X "$li$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))" + X "$ind" +} +function Emit-PlannerTimeScale { + param($ts, [string]$ind) + $cns = $script:CHART_NS + $ci = "$ind`t" + X "$ind" + X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")" + $levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() } + if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт + foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci } + $transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false } + X "$ci$(PL-Bool $transp)" + X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")" + X "$ci$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")" + X "$ci$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })" + X "$ind" +} +function Emit-PlannerItem { + param($it, [string]$ind) + X "$ind" + $ii = "$ind`t" + Emit-PlannerValue (PL-Get $it 'value' $null) $ii + Emit-PlannerText 'text' (PL-Get $it 'text' '') $ii + Emit-PlannerText 'tooltip' (PL-Get $it 'tooltip' '') $ii + X "$ii$(PL-Get $it 'begin' '0001-01-01T00:00:00')" + X "$ii$(PL-Get $it 'end' '0001-01-01T00:00:00')" + Emit-PlannerColor 'borderColor' $it 'borderColor' $ii + Emit-PlannerColor 'backColor' $it 'backColor' $ii + Emit-PlannerColor 'textColor' $it 'textColor' $ii + Emit-PlannerFont $it $ii + X "$ii" + X "$ii$(PL-Get $it 'replacementDate' '0001-01-01T00:00:00')" + X "$ii$(PL-Bool (PL-Get $it 'deleted' $false))" + $id = PL-Get $it 'id' $null + if ($null -eq $id) { $id = [guid]::NewGuid().ToString() } + X "$ii$id" + X "$ii$(PL-Bool (PL-Get $it 'textFormatted' $false))" + Emit-PlannerBorder $it $ii 'border' + X "$ii$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")" + X "$ind" +} +# Элемент измерения ( внутри ) — рекурсивен: может нести вложенные +# элементы (UI: колонка «Элементы» у элемента). Порядок: value, text, цвета, font, +# вложенные элементы, showOnlySubordinatesAreas, textFormatted. +function Emit-PlannerDimElement { + param($el, [string]$ind) + X "$ind" + $ii = "$ind`t" + Emit-PlannerValue (PL-Get $el 'value' $null) $ii + Emit-PlannerText 'text' (PL-Get $el 'text' '') $ii + Emit-PlannerColor 'borderColor' $el 'borderColor' $ii + Emit-PlannerColor 'backColor' $el 'backColor' $ii + Emit-PlannerColor 'textColor' $el 'textColor' $ii + Emit-PlannerFont $el $ii + foreach ($sub in @(PL-Get $el 'elements' @())) { Emit-PlannerDimElement $sub $ii } + X "$ii$(PL-Bool (PL-Get $el 'showOnlySubordinatesAreas' $true))" + X "$ii$(PL-Bool (PL-Get $el 'textFormatted' $false))" + X "$ind" +} +# Измерение планировщика () — объект разреза + его элементы. +function Emit-PlannerDimension { + param($d, [string]$ind) + X "$ind" + $di = "$ind`t" + Emit-PlannerValue (PL-Get $d 'value' $null) $di + Emit-PlannerText 'text' (PL-Get $d 'text' '') $di + Emit-PlannerColor 'borderColor' $d 'borderColor' $di + Emit-PlannerColor 'backColor' $d 'backColor' $di + Emit-PlannerColor 'textColor' $d 'textColor' $di + Emit-PlannerFont $d $di + foreach ($el in @(PL-Get $d 'elements' @())) { Emit-PlannerDimElement $el $di } + X "$di$(PL-Bool (PL-Get $d 'textFormatted' $false))" + X "$ind" +} +function Emit-PlannerSettings { + param($pl, [string]$ind) + X "$ind" + $si = "$ind`t" + foreach ($it in @(PL-Get $pl 'items' @())) { Emit-PlannerItem $it $si } + foreach ($d in @(PL-Get $pl 'dimensions' @())) { Emit-PlannerDimension $d $si } + Emit-PlannerColor 'borderColor' $pl 'borderColor' $si + Emit-PlannerColor 'backColor' $pl 'backColor' $si + Emit-PlannerColor 'textColor' $pl 'textColor' $si + Emit-PlannerColor 'lineColor' $pl 'lineColor' $si + Emit-PlannerFont $pl $si + X "$si$(PL-Get $pl 'beginOfRepresentationPeriod' '0001-01-01T00:00:00')" + X "$si$(PL-Get $pl 'endOfRepresentationPeriod' '0001-01-01T00:00:00')" + X "$si$(PL-Bool (PL-Get $pl 'alignElementsOfTimeScale' $true))" + X "$si$(PL-Bool (PL-Get $pl 'displayTimeScaleWrapHeaders' $true))" + X "$si$(PL-Bool (PL-Get $pl 'displayWrapHeaders' $true))" + $wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null + if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } } + Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si + X "$si$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")" + X "$si$(PL-Get $pl 'periodicVariantRepetition' 1)" + X "$si$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)" + X "$si$(PL-Get $pl 'timeScaleWrapEndIndent' 0)" + Emit-PlannerTimeScale (PL-Get $pl 'timeScale' $null) $si + $period = PL-Get $pl 'period' $null + if ($period) { + X "$si" + X "$si`t$(PL-Get $period 'begin' '0001-01-01T00:00:00')" + X "$si`t$(PL-Get $period 'end' '0001-01-01T00:00:00')" + X "$si" + } + X "$si$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))" + X "$si$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")" + X "$si$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")" + X "$si$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))" + X "$si$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))" + X "$si$(PL-Get $pl 'minColumnWidth' 0)" + X "$si$(PL-Get $pl 'minRowHeight' 0)" + X "$si$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")" + X "$si$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")" + Emit-PlannerBorder $pl $si 'border' + X "$si$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")" + X "$ind" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time — генерик-эмиттер (зеркало +# Build-ChartNode декомпилятора). Тип узла → форма XML: ML-поля (по имени), серии +# (массив, повтор тега), line/border/font (по ключам), attrs-узлы (gaugeQualityBands), +# иначе вложенный объект/скаляр. Порядок ключей = порядок эмиссии (раундтрип). +$script:CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } +$script:CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } +$script:CHART_FONT_KEYS = @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale') +function Get-Keys { param($o) if ($o -is [System.Collections.IDictionary]) { return @($o.Keys) } else { return @($o.PSObject.Properties.Name) } } +function Get-Prop { param($o, [string]$k) if ($o -is [System.Collections.IDictionary]) { return $o[$k] } else { $p = $o.PSObject.Properties[$k]; if ($p) { return $p.Value } else { return $null } } } +function Emit-ChartNode { + param([string]$name, $val, [string]$ind) + if ($script:CHART_ML_FIELDS.Contains($name)) { + if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } + X "$ind"; Emit-MLItems -val $val -indent "$ind`t"; X "$ind"; return + } + if (($val -is [System.Collections.IList]) -and ($val -isnot [string])) { + foreach ($e in $val) { Emit-ChartNode $name $e $ind } + return + } + if (($val -is [System.Management.Automation.PSCustomObject]) -or ($val -is [System.Collections.IDictionary])) { + $keys = Get-Keys $val + if ($script:CHART_ATTR_FIELDS.Contains($name)) { + $attrs = @(); foreach ($k in $keys) { $v = Get-Prop $val $k; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$k=`"$(Esc-Xml "$v")`"" } + X "$ind"; return + } + if ($keys -contains 'gap') { + $w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style' + X "$ind" + X "$ind`t$(Esc-Xml "$st")" + X "$ind"; return + } + if (($keys -contains 'style') -and ($keys -contains 'width')) { + $w = Get-Prop $val 'width'; $st = Get-Prop $val 'style' + X "$ind" + X "$ind`t$(Esc-Xml "$st")" + X "$ind"; return + } + $isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } } + if ($isFont) { + $attrs = @(); foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $v = Get-Prop $val $fk; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$fk=`"$(Esc-Xml "$v")`"" } } + X "$ind"; return + } + if (@($keys).Count -eq 0) { X "$ind"; return } + X "$ind" + foreach ($k in $keys) { Emit-ChartNode $k (Get-Prop $val $k) "$ind`t" } + X "$ind" + return + } + if ($null -eq $val -or "$val" -eq '') { X "$ind"; return } + if ($val -is [bool]) { X "$ind$(PL-Bool $val)"; return } + X "$ind$(Esc-Xml "$val")" +} +function Emit-ChartSettings { + param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart') + X "$ind" + foreach ($k in (Get-Keys $chart)) { Emit-ChartNode $k (Get-Prop $chart $k) "$ind`t" } + X "$ind" +} + +function Emit-Appearance { + param($el, [string]$indent, [string]$profile = 'field') + if ($null -eq $el) { return } + $order = switch ($profile) { + 'decoration' { $script:appOrderDecoration } + 'button' { $script:appOrderButton } + default { $script:appOrderField } + } + foreach ($key in $order) { + $val = Get-AppearanceValue -el $el -canonical $key + if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue } + $spec = $script:appearanceSpec[$key] + switch ($spec.kind) { + 'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")" } + 'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent } + 'border' { Emit-BorderTag -val $val -indent $indent } + } + } +} + +function Emit-Layout { + param($el, [string]$indent, [switch]$skipHeight, [bool]$multiLineDefault = $false) + # CommandSet (отключённые команды редактора) — общее свойство поля (input/label/check/ + # spreadsheet/html/formatted/picture); в схеме рано (после TitleLocation, перед скалярами). + if ($el.excludedCommands -and @($el.excludedCommands).Count -gt 0) { + X "$indent" + foreach ($cmd in $el.excludedCommands) { X "$indent`t$cmd" } + X "$indent" + } + Emit-CommonElementProps -el $el -indent $indent + $amwExplicit = ($el.PSObject.Properties.Name -contains 'autoMaxWidth') + if ($amwExplicit) { + if ($el.autoMaxWidth -eq $false) { X "$indentfalse" } + } elseif ($multiLineDefault) { + X "$indentfalse" + } + if ($null -ne $el.maxWidth) { X "$indent$($el.maxWidth)" } + if ($el.autoMaxHeight -eq $false) { X "$indentfalse" } + if ($null -ne $el.maxHeight) { X "$indent$($el.maxHeight)" } + if ($el.width) { X "$indent$($el.width)" } + if (-not $skipHeight -and $el.height) { X "$indent$($el.height)" } + if ($null -ne $el.horizontalStretch) { X "$indent$(if ($el.horizontalStretch){'true'}else{'false'})" } + if ($null -ne $el.verticalStretch) { X "$indent$(if ($el.verticalStretch){'true'}else{'false'})" } + if ($el.groupHorizontalAlign) { X "$indent$($el.groupHorizontalAlign)" } + if ($el.groupVerticalAlign) { X "$indent$($el.groupVerticalAlign)" } + if ($el.horizontalAlign) { X "$indent$($el.horizontalAlign)" } + Emit-GenericScalars -el $el -indent $indent +} + +function Title-FromName { + param([string]$name) + if (-not $name) { return '' } + $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') + $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') + $parts = $s -split ' ' + if ($parts.Count -eq 0) { return $s } + $out = New-Object System.Collections.ArrayList + [void]$out.Add($parts[0]) + for ($i = 1; $i -lt $parts.Count; $i++) { + $p = $parts[$i] + if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { + [void]$out.Add($p) + } else { + [void]$out.Add($p.ToLower()) + } + } + return ($out -join ' ') +} + +function Emit-Title { + # Нет ключа title → авто-вывод из имени (помощь модели). + # Явный title: "" (или null) → подавить (заголовок не эмитим). + # Явный непустой → эмитим как есть. + param($el, [string]$name, [string]$indent, [switch]$auto) + $hasKey = $null -ne $el.PSObject.Properties['title'] + if ($hasKey) { + if ($el.title) { Emit-MLText -tag "Title" -text $el.title -indent $indent } + } elseif ($auto -and $name) { + Emit-MLText -tag "Title" -text (Title-FromName -name $name) -indent $indent + } + # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. + if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } + # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. + if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } +} + +function Map-TitleLoc { + param([string]$v) + switch ("$v".ToLower()) { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + "auto" { "Auto" } + default { "$v" } + } +} + +# TitleLocation у check/radio: нет ключа → умный дефолт (Right/None), эмитится; +# "" → подавить (= дефолт платформы, она его сама не пишет); значение → эмитить (маппинг регистра). +function Emit-TitleLocation { + param($el, [string]$indent, [string]$smartDefault) + if ($null -ne $el.PSObject.Properties['titleLocation']) { + if ($el.titleLocation) { X "$indent$(Map-TitleLoc "$($el.titleLocation)")" } + } elseif ($smartDefault) { + X "$indent$smartDefault" + } +} + +function Emit-Group { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. + $groupVal = "$($el.group)".ToLower() + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwayshorizontal" { "AlwaysHorizontal" } + "alwaysvertical" { "AlwaysVertical" } + "horizontalifpossible" { "HorizontalIfPossible" } + "collapsible" { "Vertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + + # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). + # Legacy: group:'collapsible' эквивалентно behavior:'collapsible'. + $behaviorVal = if ($el.behavior) { "$($el.behavior)".ToLower() } elseif ($groupVal -eq "collapsible") { "collapsible" } else { $null } + $bmap = @{ "usual"="Usual"; "collapsible"="Collapsible"; "popup"="PopUp" } + if ($behaviorVal -and $bmap.ContainsKey($behaviorVal)) { + X "$inner$($bmap[$behaviorVal])" + } + # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) + if ($el.collapsed -eq $true) { X "$innertrue" } + + # Representation + if ($el.representation) { + $repr = switch ("$($el.representation)") { + "none" { "None" } + "normal" { "NormalSeparation" } + "weak" { "WeakSeparation" } + "strong" { "StrongSeparation" } + default { "$($el.representation)" } + } + X "$inner$repr" + } + + # ShowTitle + if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } + # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст + if ($el.collapsedTitle) { Emit-MLText -tag "CollapsedRepresentationTitle" -text $el.collapsedTitle -indent $inner } + + # United + if ($el.united -eq $false) { X "$innerfalse" } + + # Формат значения пути к данным заголовка (; парный к titleDataPath группы) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) — перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-ColumnGroup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation (horizontal / vertical / inCell — последнее только здесь) + $groupVal = "$($el.columnGroup)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "inCell" { "InCell" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + + if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } + # showInHeader эмитится общим Emit-CommonElementProps (через Emit-Layout) + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) + Emit-ColumnPics -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) — перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Input { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + default { "$($el.titleLocation)" } + } + X "$inner$loc" + } + + if ($null -ne $el.multiLine) { X "$inner$(if ($el.multiLine){'true'}else{'false'})" } + if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } + # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, + # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). + if ($null -ne $el.choiceButton) { X "$inner$(if ($el.choiceButton){'true'}else{'false'})" } + # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) + if ($null -ne $el.clearButton) { X "$inner$(if ($el.clearButton){'true'}else{'false'})" } + if ($null -ne $el.spinButton) { X "$inner$(if ($el.spinButton){'true'}else{'false'})" } + if ($null -ne $el.dropListButton) { X "$inner$(if ($el.dropListButton){'true'}else{'false'})" } + if ($null -ne $el.choiceListButton) { X "$inner$(if ($el.choiceListButton){'true'}else{'false'})" } + if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-ColumnPics -el $el -indent $inner + if ($el.textEdit -eq $false) { X "$innerfalse" } + # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) + foreach ($p in @( + @('wrap','Wrap'), @('openButton','OpenButton'), @('listChoiceMode','ListChoiceMode'), + @('extendedEditMultipleValues','ExtendedEditMultipleValues'), @('chooseType','ChooseType'), + @('quickChoice','QuickChoice'), @('autoChoiceIncomplete','AutoChoiceIncomplete') + )) { + if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})" } + } + # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. + # availableTypes — формат типа реквизита (§type); Emit-Type сам разбирает мультитип "a | b". + if ($null -ne $el.typeDomainEnabled) { X "$inner$(if ($el.typeDomainEnabled){'true'}else{'false'})" } + if ($el.availableTypes) { Emit-Type -typeStr $el.availableTypes -indent $inner -tag 'AvailableTypes' } + # InputField-специфичные value-скаляры + foreach ($p in @( + @('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'), + @('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath') + )) { + if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")" } + } + # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). + foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) { + if ($null -ne $el.($p[0])) { + $mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' } + X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")" + } + } + if ($el.choiceButtonRepresentation) { X "$inner$($el.choiceButtonRepresentation)" } + Emit-PictureRef -val $el.choiceButtonPicture -picTag 'ChoiceButtonPicture' -indent $inner + Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true)) + + if ($el.inputHint) { + Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner + } + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + Emit-ChoiceList -el $el -indent $inner + + # Связи по типу / связи параметров выбора / параметры выбора + Emit-TypeLink -el $el -indent $inner + Emit-ChoiceParameterLinks -el $el -indent $inner + Emit-ChoiceParameters -el $el -indent $inner + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" + + X "$indent" +} + +function Emit-Check { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-ColumnPics -el $el -indent $inner + # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг + if ($null -ne $el.PSObject.Properties['checkBoxType']) { + if ($el.checkBoxType) { + $cbt = switch ("$($el.checkBoxType)".ToLower()) { 'auto' {'Auto'} 'checkbox' {'CheckBox'} 'switcher' {'Switcher'} 'tumbler' {'Tumbler'} default {"$($el.checkBoxType)"} } + X "$inner$cbt" + } + } else { X "$innerAuto" } + + Emit-TitleLocation -el $el -indent $inner -smartDefault "Right" + + Emit-Layout -el $el -indent $inner + + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" + + X "$indent" +} + +# Maps Russian/English root of a typed reference path to canonical English root. +# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y". +$script:refRootSynonyms = @{ + "Перечисление" = "Enum" + "Справочник" = "Catalog" + "Документ" = "Document" + "ПланСчетов" = "ChartOfAccounts" + "ПланВидовХарактеристик" = "ChartOfCharacteristicTypes" + "ПланВидовРасчета" = "ChartOfCalculationTypes" + "ПланВидовРасчёта" = "ChartOfCalculationTypes" + "ПланОбмена" = "ExchangePlan" + "БизнесПроцесс" = "BusinessProcess" + "Задача" = "Task" + "РегистрСведений" = "InformationRegister" + "РегистрНакопления" = "AccumulationRegister" + "РегистрБухгалтерии" = "AccountingRegister" + "РегистрРасчета" = "CalculationRegister" + "РегистрРасчёта" = "CalculationRegister" + "ЖурналДокументов" = "DocumentJournal" + "КритерийОтбора" = "FilterCriterion" +} +$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления") + +# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты". +# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений. +function Normalize-MetaTypeRef { + param([string]$ref) + if ([string]::IsNullOrEmpty($ref)) { return $ref } + $dot = $ref.IndexOf('.') + if ($dot -lt 1) { return $ref } + $root = $ref.Substring(0, $dot) + if ($script:refRootSynonyms.ContainsKey($root)) { + return $script:refRootSynonyms[$root] + $ref.Substring($dot) + } + return $ref +} + +# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." } +function Normalize-ChoiceValue { + param($value) + + # Booleans + if ($value -is [bool]) { + return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } } + } + # Numbers (int / decimal / double) + if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) { + return @{ XsiType = "xs:decimal"; Text = "$value" } + } + + $s = "$value" + if ([string]::IsNullOrEmpty($s)) { + return @{ XsiType = "xs:string"; Text = "" } + } + + # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime + if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { + return @{ XsiType = "xs:dateTime"; Text = $s } + } + + # Try to detect typed reference path: ".[..]" + $parts = $s -split '\.' + if ($parts.Count -ge 2) { + $root = $parts[0] + $canonRoot = $null + if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] } + elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root } + + if ($canonRoot) { + $typeName = $parts[1] + $normalized = $null + + if ($canonRoot -eq "Enum") { + if ($parts.Count -eq 2) { + # "Enum.X" alone — not a value, treat as string + } elseif ($parts.Count -eq 3) { + # "Enum.X.Y" — insert .EnumValue. ("EmptyRef" — пустая ссылка, БЕЗ вставки) + if ($parts[2] -eq 'EmptyRef') { $normalized = "Enum.$typeName.EmptyRef" } + else { $normalized = "Enum.$typeName.EnumValue.$($parts[2])" } + } else { + # "Enum.X..Y..." — replace member with EnumValue (handles ЗначениеПеречисления too) + $member = $parts[2] + if ($script:enumValueSynonyms -contains $member) { + $rest = $parts[3..($parts.Count-1)] -join '.' + $normalized = "Enum.$typeName.EnumValue.$rest" + } else { + $rest = $parts[2..($parts.Count-1)] -join '.' + $normalized = "Enum.$typeName.EnumValue.$rest" + } + } + } else { + # Other ref roots: just translate root, keep tail as-is + if ($parts.Count -ge 3) { + $tail = $parts[1..($parts.Count-1)] -join '.' + $normalized = "$canonRoot.$tail" + } + } + + if ($normalized) { + return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized } + } + } + } + + return @{ XsiType = "xs:string"; Text = $s } +} + +# Emit Presentation block for a choiceList item. +# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}. +# Empty/null → emits empty . +function Emit-ChoicePresentation { + param($pres, [string]$indent) + if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) { + X "$indent" + return + } + + $pairs = @() + if ($pres -is [string]) { + $pairs += ,@("ru", $pres) + } elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) { + foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") } + } elseif ($pres.PSObject -and $pres.PSObject.Properties) { + foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") } + } else { + $pairs += ,@("ru", "$pres") + } + + X "$indent" + foreach ($pair in $pairs) { + X "$indent`t" + X "$indent`t`t$($pair[0])" + X "$indent`t`t$(Esc-Xml $pair[1])" + X "$indent`t" + } + X "$indent" +} + +# для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). +function Get-ChoiceValueTag { + param($norm) + if ([string]::IsNullOrEmpty($norm.Text)) { return "" } + return "$(Esc-Xml $norm.Text)" +} + +# Emit (список выбора) — у RadioButtonField и InputField. +# Элемент: { value, presentation?/title? } (+ рус. синонимы значение/представление). +function Emit-ChoiceList { + param($el, [string]$indent) + if (-not $el.choiceList -or $el.choiceList.Count -eq 0) { return } + X "$indent" + $itemIndent = "$indent`t" + foreach ($item in $el.choiceList) { + # value (+ рус. синоним "значение") + $valRaw = $null + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("value")) { $valRaw = $item["value"] } + elseif ($item.Contains("значение")) { $valRaw = $item["значение"] } + } else { + if ($item.PSObject.Properties["value"]) { $valRaw = $item.value } + elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" } + } + + # presentation (presentation OR title синоним) + $presRaw = $null + $hasPres = $false + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true } + elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true } + elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true } + } else { + if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true } + elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true } + elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true } + } + + # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — + # переопределяет авто-детект (Normalize-ChoiceValue вывела бы xs:string). + $vtRaw = $null + if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) { + if ($item.Contains("valueType")) { $vtRaw = "$($item["valueType"])" } + } elseif ($item.PSObject.Properties["valueType"]) { $vtRaw = "$($item.valueType)" } + + if ($vtRaw) { $norm = @{ XsiType = $vtRaw; Text = "$valRaw" } } + else { $norm = Normalize-ChoiceValue -value $valRaw } + + # авто-вывод presentation, если не задан + if (-not $hasPres) { + if ($norm.XsiType -eq "xr:DesignTimeRef") { + $tail = ($norm.Text -split '\.')[-1] + $presRaw = Title-FromName -name $tail + } else { + $presRaw = $norm.Text + } + } + + X "$itemIndent" + $valIndent = "$itemIndent`t" + X "$valIndent" + X "$valIndent0" + X "$valIndent" + Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t" + X "$valIndent`t$(Get-ChoiceValueTag $norm)" + X "$valIndent" + X "$itemIndent" + } + X "$indent" +} + +# Чтение свойства из hashtable/PSCustomObject по списку синонимов (первый найденный, иначе $null). +function Get-ElProp { + param($obj, [string[]]$names) + if ($null -eq $obj) { return $null } + foreach ($n in $names) { + if ($obj -is [System.Collections.IDictionary]) { + if ($obj.Contains($n)) { return $obj[$n] } + } elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { + return $obj.PSObject.Properties[$n].Value + } + } + return $null +} + +# Приведение строкового литерала shorthand к типу: true/false → bool, целое/дробное → число, +# иначе строка (ref-путь нормализует уже Normalize-ChoiceValue). +function ConvertTo-ScalarLiteral { + param([string]$s) + $t = "$s".Trim() + if ($t -match '^(?i:true)$') { return $true } + if ($t -match '^(?i:false)$') { return $false } + if ($t -match '^-?\d+$') { return [int]$t } + if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) } + return $t +} + +# Shorthand параметра выбора: "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. +function ConvertFrom-ChoiceParamShorthand { + param([string]$s) + $eq = $s.IndexOf('=') + if ($eq -lt 0) { return @{ name = $s.Trim() } } + $name = $s.Substring(0, $eq).Trim() + $rest = $s.Substring($eq + 1) + if ($rest -match ',') { + $vals = @() + foreach ($part in ($rest -split ',')) { $vals += ,(ConvertTo-ScalarLiteral $part) } + return @{ name = $name; value = $vals } + } + return @{ name = $name; value = (ConvertTo-ScalarLiteral $rest) } +} + +# Shorthand связи параметров выбора: "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. +function ConvertFrom-ChoiceParamLinkShorthand { + param([string]$s) + $eq = $s.IndexOf('=') + if ($eq -lt 0) { return @{ name = $s.Trim() } } + $o = @{ name = $s.Substring(0, $eq).Trim() } + $rest = $s.Substring($eq + 1).Trim() + if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { + $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] + } else { + $o['dataPath'] = $rest + } + return $o +} + +# Shorthand связи по типу: "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. +function ConvertFrom-TypeLinkShorthand { + param([string]$s) + if ($s -match '^(.*)#(\d+)$') { return @{ dataPath = $matches[1].Trim(); linkItem = [int]$matches[2] } } + return @{ dataPath = "$s".Trim() } +} + +# Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . +# Скаляр → один Value (через Normalize-ChoiceValue); массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. +function Emit-ChoiceParamValue { + # $isArray передаётся ЯВНО из вызывающего кода: PowerShell разворачивает одноэлементный массив + # при биндинге параметра ($value становится скаляром), поэтому определять массив тут — ненадёжно + # (1-элементный список `["X"]` эмитился бы скаляром вместо FixedArray). foreach по скаляру = 1 итерация. + param($value, [string]$indent, [bool]$isArray) + X "$indent" + if ($isArray) { + X "$indent" + foreach ($v in $value) { + $norm = Normalize-ChoiceValue -value $v + X "$indent`t" + X "$indent`t`t" + X "$indent`t`t$(Get-ChoiceValueTag $norm)" + X "$indent`t" + } + X "$indent" + } else { + $norm = Normalize-ChoiceValue -value $value + X "$indent$(Get-ChoiceValueTag $norm)" + } +} + +# (параметры выбора поля ввода) — [{name, value}]. value через Normalize-ChoiceValue; +# массив значений → FixedArray. Рус. синонимы имя/значение. +function Emit-ChoiceParameters { + param($el, [string]$indent) + $cp = $el.choiceParameters + if (-not $cp -or @($cp).Count -eq 0) { return } + X "$indent" + foreach ($item in @($cp)) { + if ($item -is [string]) { $item = ConvertFrom-ChoiceParamShorthand $item } + $name = Get-ElProp $item @('name','имя') + # Наличие ключа value (≠ значения) + ПРЯМОЙ доступ к значению (без Get-ElProp): его return + # разворачивает 1-элементный массив (PS unwrap), теряя массив-ность → FixedArray не эмитится. + # Индексер/member-доступ массив сохраняет; if-выражение/функция-return — нет. + $hasVal = $false; $val = $null + if ($item -is [System.Collections.IDictionary]) { + if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] } + elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] } + } else { + if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value } + elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value } + } + $valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string]) + X "$indent`t" + # Параметр выбора без значения → (платформа, 13 в корпусе); + # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. + if (-not $hasVal) { + X "$indent`t`t" + } else { + X "$indent`t`t" + Emit-ChoiceParamValue -value $val -indent "$indent`t`t`t" -isArray $valIsArray + X "$indent`t`t" + } + X "$indent`t" + } + X "$indent" +} + +# (связи параметров выбора) — [{name, dataPath, valueChange?}]. +# valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. +function Emit-ChoiceParameterLinks { + param($el, [string]$indent) + $cpl = $el.choiceParameterLinks + if (-not $cpl -or @($cpl).Count -eq 0) { return } + X "$indent" + foreach ($lk in @($cpl)) { + if ($lk -is [string]) { $lk = ConvertFrom-ChoiceParamLinkShorthand $lk } + $name = Get-ElProp $lk @('name','имя') + $dp = Get-ElProp $lk @('dataPath','path','путь') + $vcRaw = Get-ElProp $lk @('valueChange','режимИзменения') + $vc = "Clear" + if ($vcRaw) { + $vc = switch -Regex ("$vcRaw".ToLower()) { + '^(clear|очистить|очистка)$' { "Clear"; break } + '^(dontchange|неизменять|неменять|нет)$' { "DontChange"; break } + default { "$vcRaw" } + } + } + X "$indent`t" + X "$indent`t`t$(Esc-Xml "$name")" + X "$indent`t`t$(Esc-Xml "$dp")" + X "$indent`t`t$vc" + X "$indent`t" + } + X "$indent" +} + +# (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. +function Emit-TypeLink { + param($el, [string]$indent) + $tl = $el.typeLink + if (-not $tl) { return } + if ($tl -is [string]) { $tl = ConvertFrom-TypeLinkShorthand $tl } + $dp = Get-ElProp $tl @('dataPath','path','путь') + $li = Get-ElProp $tl @('linkItem','элементСвязи') + if ($null -eq $li) { $li = 0 } + X "$indent" + X "$indent`t$(Esc-Xml "$dp")" + X "$indent`t$li" + X "$indent" +} + +function Emit-Radio { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-TitleLocation -el $el -indent $inner -smartDefault "None" + + # RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms. + $rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" } + $rbt = switch -Regex ($rbtRaw.ToLower()) { + '^(auto|авто)$' { "Auto"; break } + '^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break } + '^(tumbler|тумблер)$' { "Tumbler"; break } + default { $rbtRaw } + } + X "$inner$rbt" + + if ($null -ne $el.columnsCount) { + X "$inner$($el.columnsCount)" + } + + Emit-ChoiceList -el $el -indent $inner + + Emit-Layout -el $el -indent $inner + + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio" + + X "$indent" +} + +# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму +# (reuse Resolve-MLFormatted, как у extendedTooltip). Атрибут formatted эмитится ВСЕГДА +# (специфика декораций). Sibling-ключ `formatted` — back-compat override авто-детекта. +function Emit-DecorationTitle { + param($el, [string]$name, [string]$indent, [switch]$auto) + $hasKey = $null -ne $el.PSObject.Properties['title'] + $titleVal = if ($hasKey) { $el.title } elseif ($auto -and $name) { Title-FromName -name $name } else { $null } + if ($titleVal) { + $r = Resolve-MLFormatted $titleVal + $fmt = if ($null -ne $el.PSObject.Properties['formatted']) { [bool]$el.formatted } else { $r.formatted } + X "$indent<Title formatted=`"$(if ($fmt) { 'true' } else { 'false' })`">" + Emit-MLItems -val $r.text -indent "$indent`t" + X "$indent" + } + if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent } + if ($el.tooltipRepresentation) { X "$indent$($el.tooltipRepresentation)" } +} + +function Emit-Label { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title + # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). + Emit-CommonFlags -el $el -indent $inner + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + Emit-Appearance -el $el -indent $inner -profile 'decoration' + + Emit-DecorationTitle -el $el -name $name -indent $inner -auto + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" + + X "$indent" +} + +function Emit-LabelField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + if ($el.editMode) { X "$inner$($el.editMode)" } + # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode + if ($el.footerDataPath) { X "$inner$(Esc-Xml "$($el.footerDataPath)")" } + # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение + if ($null -ne $el.passwordMode) { X "$inner$(if ($el.passwordMode){'true'}else{'false'})" } + Emit-ColumnPics -el $el -indent $inner + # ВНИМАНИЕ: у LabelField платформенный тег именно (опечатка 1С), не . + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + + if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner } + if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner } + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + + # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" + + X "$indent" +} + +# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). +# Платформа всегда эмитит этот блок на дин-список-таблице; компилятор зеркалит дефолты, +# DSL-ключ переопределяет; декомпилятор инвертирует (опускает значения = дефолту). +function Emit-DynListTableBlock { + param($el, [string]$indent) + # (useAlternationRowColor — общее свойство таблицы, эмитится в Emit-Table) + # Group A (гарант. блок, n=5079): дефолт + override + $ar = if ($el.autoRefresh -eq $true) { "true" } else { "false" } + X "$indent$ar" + $arp = if ($el.PSObject.Properties["autoRefreshPeriod"] -and $null -ne $el.autoRefreshPeriod) { $el.autoRefreshPeriod } else { 60 } + X "$indent$arp" + X "$indent" + X "$indent`tCustom" + X "$indent`t0001-01-01T00:00:00" + X "$indent`t0001-01-01T00:00:00" + X "$indent" + $cfi = if ($el.choiceFoldersAndItems) { $el.choiceFoldersAndItems } else { "Items" } + X "$indent$cfi" + $rcr = if ($el.restoreCurrentRow -eq $true) { "true" } else { "false" } + X "$indent$rcr" + X "$indent" + $sr = if ($el.showRoot -eq $false) { "false" } else { "true" } + X "$indent$sr" + $arc = if ($el.allowRootChoice -eq $true) { "true" } else { "false" } + X "$indent$arc" + $uodc = if ($el.updateOnDataChange) { $el.updateOnDataChange } else { "Auto" } + X "$indent$uodc" + if ($el.userSettingsGroup) { X "$indent$($el.userSettingsGroup)" } + $agcru = if ($el.allowGettingCurrentRowURL -eq $false) { "false" } else { "true" } + X "$indent$agcru" +} + +function Emit-Table { + param($el, [string]$name, [int]$id, [string]$indent) + + $script:currentTableName = $name # дефолт source для кастомных дополнений в commandBar + X "$indent
" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + # ChangeRowSet/Order — эмитим явное значение (в т.ч. false: платформа пишет его на ValueTable) + if ($el.PSObject.Properties['changeRowSet'] -and $null -ne $el.changeRowSet) { + X "$inner$(if ($el.changeRowSet -eq $true){'true'}else{'false'})" + } + if ($el.PSObject.Properties['changeRowOrder'] -and $null -ne $el.changeRowOrder) { + X "$inner$(if ($el.changeRowOrder -eq $true){'true'}else{'false'})" + } + if ($el.autoInsertNewRow -eq $true) { X "$innertrue" } + # RowFilter — nil-плейсхолдер (всегда пустой); ключ присутствует → эмитим + if ($el.PSObject.Properties['rowFilter']) { X "$inner" } + # Высота в строках таблицы () — отдельное свойство от (высота элемента, + # эмитится generic-ом Emit-Layout ниже). Таблица может нести оба (237 в корпусе). + if ($el.heightInTableRows) { X "$inner$($el.heightInTableRows)" } + if ($el.header -eq $false) { X "$inner
false
" } + if ($el.footer -eq $true) { X "$inner
true
" } + + if ($el.commandBarLocation) { + X "$inner$($el.commandBarLocation)" + } + if ($el.searchStringLocation) { + X "$inner$($el.searchStringLocation)" + } + if ($el.choiceMode -eq $true) { X "$innertrue" } + # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). + if ($null -ne $el.autofill) { X "$inner$(if ($el.autofill){'true'}else{'false'})" } + if ($el.multipleChoice -eq $true) { X "$innertrue" } + if ($el.searchOnInput) { X "$inner$($el.searchOnInput)" } + if ($null -ne $el.markIncomplete) { X "$inner$(if ($el.markIncomplete){'true'}else{'false'})" } + # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) + if ($null -ne $el.headerHeight) { X "$inner$($el.headerHeight)" } + if ($null -ne $el.footerHeight) { X "$inner$($el.footerHeight)" } + if ($el.useAlternationRowColor -eq $true) { X "$innertrue" } + if ($el.selectionMode) { X "$inner$($el.selectionMode)" } + if ($el.rowSelectionMode) { X "$inner$($el.rowSelectionMode)" } + if ($el.verticalLines -eq $false) { X "$innerfalse" } + if ($el.horizontalLines -eq $false) { X "$innerfalse" } + if ($el.initialTreeView) { X "$inner$($el.initialTreeView)" } + if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } + if ($el.rowPictureDataPath) { X "$inner$($el.rowPictureDataPath)" } + # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) + Emit-PictureRef -val $el.rowsPicture -picTag 'RowsPicture' -indent $inner + # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) + if ($el.currentRowUse) { X "$inner$($el.currentRowUse)" } + # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) + if ($el.refreshRequest) { X "$inner$($el.refreshRequest)" } + # Блок свойств дин-список-таблицы (помечена эвристикой 11b.4) + if ($el.PSObject.Properties["_dynList"] -and $el._dynList) { Emit-DynListTableBlock -el $el -indent $inner } + if ($el.viewStatusLocation) { X "$inner$($el.viewStatusLocation)" } + if ($el.searchControlLocation) { X "$inner$($el.searchControlLocation)" } + Emit-Layout -el $el -indent $inner + + # CommandSet таблицы эмитится через Emit-Layout (общий механизм поля) + + # Оформление (цвета/граница таблицы) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + # AutoCommandBar: приоритет commandBar-свойства (контент); иначе tableAutofill-shorthand; иначе пусто. + if ($null -ne $el.commandBar) { + Emit-CompanionPanel -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner -panel $el.commandBar + } elseif ($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 "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + $adds = $el.additions + Emit-TableAddition -typeKey 'searchString' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchString') + Emit-TableAddition -typeKey 'viewStatus' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'viewStatus') + Emit-TableAddition -typeKey 'searchControl' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchControl') + + # Columns + if ($el.columns -and $el.columns.Count -gt 0) { + X "$inner" + foreach ($col in $el.columns) { + Emit-Element -el $col -indent "$inner`t" + } + X "$inner" + } + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" + + X "$indent
" +} + +function Emit-Pages { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.pagesRepresentation) { + X "$inner$($el.pagesRepresentation)" + } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" + + # Children (pages) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Page { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner -auto + Emit-CommonFlags -el $el -indent $inner + + # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). + # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. + Emit-PictureRef -val $el.picture -picTag 'Picture' -indent $inner + + if ($el.group) { + # Доступные значения страницы/обычной группы: Vertical / HorizontalIfPossible / AlwaysHorizontal + # (InCell — только у columnGroup). Horizontal/AlwaysVertical оставлены forgiving (legacy). + $orientation = switch ("$($el.group)") { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } + "alwaysVertical" { "AlwaysVertical" } + "horizontalIfPossible" { "HorizontalIfPossible" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + } + if ($null -ne $el.showTitle) { X "$inner$(if ($el.showTitle){'true'}else{'false'})" } + # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) + if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner } + if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner } + Emit-Layout -el $el -indent $inner + + # Оформление страницы (BackColor / TitleTextColor / TitleFont) — после ShowTitle, перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Button { + param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false) + + X "$indent" +} + +function Emit-PictureDecoration { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-DecorationTitle -el $el -name $name -indent $inner + # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) + if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } + Emit-CommonFlags -el $el -indent $inner + + # Источник картинки — ТОЛЬКО $el.src (у PictureDecoration ключ 'picture' = тип/имя элемента, не источник). + # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . + if ($el.src) { + $srcStr = "$($el.src)" + $lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" } + X "$inner" + if ($srcStr -match '^abs:(.*)$') { X "$inner`t$(Esc-Xml $matches[1])" } + else { X "$inner`t$(Esc-Xml $srcStr)" } + X "$inner`t$lt" + if ($el.transparentPixel) { X "$inner`t" } + X "$inner" + } + + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Layout -el $el -indent $inner + + # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) + Emit-Appearance -el $el -indent $inner -profile 'decoration' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" + + X "$indent" +} + +function Emit-PictureField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.editMode) { X "$inner$($el.editMode)" } + Emit-ColumnPics -el $el -indent $inner + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + if ($el.hyperlink -eq $true) { X "$innertrue" } + + Emit-Layout -el $el -indent $inner + + # ValuesPicture — picture (collection) used to render the field's value. + # Required for a Boolean-bound PictureField to actually show an icon. + # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. + Emit-PictureRef -val $el.valuesPicture -picTag 'ValuesPicture' -indent $inner + if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" + + X "$indent" +} + +function Emit-Calendar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + "auto" { "Auto" } + default { "$($el.titleLocation)" } + } + X "$inner$loc" + } + + Emit-Layout -el $el -indent $inner + + # Календарно-специфичные свойства (порядок схемы: после layout, до companions) + if ($el.selectionMode) { X "$inner$($el.selectionMode)" } + if ($null -ne $el.showCurrentDate) { $v = if ($el.showCurrentDate) { "true" } else { "false" }; X "$inner$v" } + if ($null -ne $el.widthInMonths) { X "$inner$($el.widthInMonths)" } + if ($null -ne $el.heightInMonths) { X "$inner$($el.heightInMonths)" } + if ($null -ne $el.showMonthsPanel) { $v = if ($el.showMonthsPanel) { "true" } else { "false" }; X "$inner$v" } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" + + X "$indent" +} + +# Спец-поля «документ/датчик» (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): +# единый скелет поля (path/title/flags/titleLocation/editMode/layout/companions/events). +# Типоспец. enum/bool скаляры — через generic (Emit-Layout→Emit-GenericScalars); +# числовые скаляры датчиков (min/max/шаги) — без xsi:type (≠ типизированных InputField). +# enableDrag/enableStartDrag — общие (Emit-CommonElementProps), фактическое значение. +function Emit-SimpleField { + param($el, [string]$name, [int]$id, [string]$indent, [string]$xmlTag, [string]$typeKey) + + X "$indent<$xmlTag name=`"$name`" id=`"$id`"$(DI-Attr $el)>" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + if ($el.editMode) { X "$inner$($el.editMode)" } + + Emit-Layout -el $el -indent $inner + + # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через Emit-Layout. + if ($null -ne $el.enableDrag) { X "$inner$(if ($el.enableDrag){'true'}else{'false'})" } + + # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) + foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'), @('largeStep','LargeStep'), @('markingStep','MarkingStep'), @('step','Step'))) { + if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$($el.($p[0]))" } + } + + # Оформление (цвета/шрифты/граница) — перед компаньонами + Emit-Appearance -el $el -indent $inner -profile 'field' + + # Companions + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + Emit-Events -el $el -elementName $name -indent $inner -typeKey $typeKey + + X "$indent" +} + +# GanttChartField — скелет поля + вложенная (полноценная таблица, через Emit-Element). +# Порядок (по корпусу): path/title/flags/titleLocation/layout/appearance, companions, Table, events. +function Emit-GanttChart { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path) + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { X "$inner$(Map-TitleLoc "$($el.titleLocation)")" } + Emit-Layout -el $el -indent $inner + Emit-Appearance -el $el -indent $inner -profile 'field' + Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем Emit-Element) + if ($el.ganttTable) { Emit-Element -el $el.ganttTable -indent $inner } + Emit-Events -el $el -elementName $name -indent $inner -typeKey "ganttChart" + X "$indent" +} + +function Emit-CommandBar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.commandSource) { X "$inner$($el.commandSource)" } + + if ($el.autofill -eq $true) { X "$innertrue" } + + $hl = Get-HLocation $el; if ($hl) { X "$inner$hl" } + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +function Emit-ButtonGroup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + if ($el.commandSource) { X "$inner$($el.commandSource)" } + + if ($el.representation) { + X "$inner$($el.representation)" + } + + Emit-CommonFlags -el $el -indent $inner + Emit-Layout -el $el -indent $inner + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children (кнопки в контексте командной панели) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +function Emit-Popup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner -auto + Emit-CommonFlags -el $el -indent $inner + + Emit-CommandPicture -pic $el.picture -elemLt $el.loadTransparent -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + Emit-Layout -el $el -indent $inner + + # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном + Emit-Appearance -el $el -indent $inner -profile 'field' + + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" -inCmdBar $true + } + X "$inner" + } + + X "$indent" +} + +# --- 8. Attribute emitter --- + +# FunctionalOption.X — у Attribute/Command/Column. +# DSL: массив строк. Forgiving: "X" / "FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. +function Emit-FunctionalOptions { + param($fo, [string]$indent) + if (-not $fo -or @($fo).Count -eq 0) { return } + X "$indent" + foreach ($opt in @($fo)) { + $v = "$opt" + if ($v -match '^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$') { } # GUID — как есть + elseif ($v -match '^FunctionalOption\.') { } # уже с префиксом + else { $v = "FunctionalOption.$v" } + X "$indent`t$v" + } + X "$indent" +} + +# Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. +function Emit-AttrColumn { + param($col, [string]$indent) + $colId = New-Id + X "$indent" + if ($col.title) { Emit-MLText -tag "Title" -text $col.title -indent "$indent`t" } + Emit-Type -typeStr "$($col.type)" -indent "$indent`t" + Emit-FunctionalOptions -fo $col.functionalOptions -indent "$indent`t" + # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита + if ($null -ne $col.view) { Emit-XrFlag -tag 'View' -val $col.view -indent "$indent`t" } + if ($null -ne $col.edit) { Emit-XrFlag -tag 'Edit' -val $col.edit -indent "$indent`t" } + X "$indent" +} + +# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- +# Та же сущность, что параметры СКД (см. skd-compile), но в форме: обёртка +# + дети с префиксом dcssch:. DSL переиспользует грамматику параметров СКД (shorthand + +# объект). Контекстные дефолты дин-списка (паттерн «умный дефолт у всегда-эмитируемого тега»): +# useRestriction — эмитим ВСЕГДА, дефолт TRUE (в СКД дефолт false); +# title — авто из имени (Title-FromName), если ключ не задан. +# Канон. порядок детей (по корпусу acc/erp 8.3.24): name, title, valueType, value, +# useRestriction, expression, availableValue*, valueListAllowed, availableAsField, +# inputParameters, denyIncompleteValues, use. + +# Многоязычный текст в DCS-контексте — с xsi:type="v8:LocalStringType" (form-compile +# Emit-MLText его НЕ ставит, т.к. формовые голые; в dcssch:* — обязателен). +function Emit-DLMLText { + param([string]$tag, $text, [string]$indent) + X "$indent<$tag xsi:type=`"v8:LocalStringType`">" + Emit-MLItems -val $text -indent "$indent`t" + X "$indent</$tag>" +} + +function Has-DLProp { + param($obj, [string]$name) + if ($null -eq $obj) { return $false } + if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } + if ($obj.PSObject -and $obj.PSObject.Properties[$name]) { return $true } + return $false +} + +function Split-DLValueListCsv { + param([string]$s) + $result = @() + if ($null -eq $s) { return ,$result } + $items = @(); $buf = New-Object System.Text.StringBuilder; $inQuote = $null + for ($i = 0; $i -lt $s.Length; $i++) { + $ch = $s[$i] + if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } + elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } + elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } + else { [void]$buf.Append($ch) } + } + if ($buf.Length -gt 0) { $items += $buf.ToString() } + foreach ($raw in $items) { + $t = $raw.Trim() + if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } + if ($t -ne "") { $result += $t } + } + return ,$result +} + +# Shorthand: "Имя [Заголовок]: Тип = Значение @valueList @hidden" +function Parse-DLParamShorthand { + param([string]$s) + $result = @{ name = ""; type = ""; value = $null; title = $null } + if ($s -match '@valueList') { $result.valueListAllowed = $true; $s = $s -replace '\s*@valueList', '' } + if ($s -match '@hidden') { $result.hidden = $true; $s = $s -replace '\s*@hidden', '' } + if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim(); $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } + # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). + if ($s -match '^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$') { + $result.name = $Matches[1].Trim() + $typeRaw = $Matches[2].Trim() + if ($typeRaw -match '[|+]') { + $result.type = (($typeRaw -split '\s*[|+]\s*') | ForEach-Object { Resolve-TypeStr ($_.Trim()) }) -join ' | ' + } else { + $result.type = Resolve-TypeStr $typeRaw + } + if ($Matches[4]) { + $rhs = $Matches[4].Trim() + $items = Split-DLValueListCsv $rhs + if ($items.Count -ge 2) { $result.value = $items; $result.valueListAllowed = $true } + elseif ($items.Count -eq 1) { $result.value = $items[0] } + else { $result.value = $rhs } + } + } else { $result.name = $s.Trim() } + return $result +} + +function Test-DLEmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "" -or $s -eq "_" -or $s.ToLowerInvariant() -eq "null") { return $true } + return $false +} + +# Эмиссия <dcssch:value> по типу/значению (xs:* или dcscor:DesignTimeValue для ссылок). +function Emit-DLValue { + param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) + if (Test-DLEmptyValue $val) { + # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil, даже при известном + # типе (в отличие от типизированного пустого в параметрах отчёта СКД). + if ($valueListAllowed) { return } + X "$indent<dcssch:value xsi:nil=`"true`"/>" + return + } + $valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" } + if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } + else { + if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" } + elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" } + else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" } + } +} + +# <dcssch:valueType> — обёртка + тело через Emit-SingleType (ref-типы → cfg:, как в форме). +function Emit-DLValueType { + param($typeStr, [string]$indent) + if (-not $typeStr) { return } + X "$indent<dcssch:valueType>" + $parts = "$typeStr" -split '\s*[|+]\s*' + foreach ($part in $parts) { Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" } + X "$indent</dcssch:valueType>" +} + +function Emit-DLAvailableValue { + param($av, [string]$type, [string]$indent) + X "$indent<dcssch:availableValue>" + $avVal = if (Has-DLProp $av 'value') { $av.value } else { $null } + Emit-DLValue -type $type -val $avVal -indent "$indent`t" -valueListAllowed $false + $pres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { $null } + if ($pres) { Emit-DLMLText -tag "dcssch:presentation" -text $pres -indent "$indent`t" } + X "$indent</dcssch:availableValue>" +} + +# <dcssch:inputParameters> — ChoiceParameters / ChoiceParameterLinks / простое значение (порт из skd). +function Emit-DLInputParameters { + param($ip, [string]$indent) + if ($null -eq $ip) { return } + $items = @($ip) + if ($items.Count -eq 0) { return } + X "$indent<dcssch:inputParameters>" + foreach ($item in $items) { + X "$indent`t<dcscor:item>" + if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } + X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>" + if (Has-DLProp $item 'choiceParameters') { + $cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() } + if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" } + else { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">" + foreach ($cpItem in $cpItems) { + X "$indent`t`t`t<dcscor:item>" + X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>" + foreach ($v in @($cpItem.values)) { + if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" } + elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" } + else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" } + } + X "$indent`t`t`t</dcscor:item>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif (Has-DLProp $item 'choiceParameterLinks') { + $cplItems = if ($null -ne $item.choiceParameterLinks) { @($item.choiceParameterLinks) } else { @() } + if ($cplItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" } + else { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">" + foreach ($cplItem in $cplItems) { + X "$indent`t`t`t<dcscor:item>" + X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>" + X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>" + $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } + X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>" + X "$indent`t`t`t</dcscor:item>" + } + X "$indent`t`t</dcscor:value>" + } + } elseif (Has-DLProp $item 'value') { + $val = $item.value + if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" } + elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" } + elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } + else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" } + } + X "$indent`t</dcscor:item>" + } + X "$indent</dcssch:inputParameters>" +} + +# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd-compile ── +# Грамматика идентична СКД: shorthand "Имя = Значение @off @user" или объект +# {parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation?}. +function Test-EmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "") { return $true } + if ($s -eq "_") { return $true } + if ($s.ToLowerInvariant() -eq "null") { return $true } + return $false +} +function Emit-EmptyValue { + param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) + if ($valueListAllowed) { return } + $t = if ($null -eq $type) { "" } else { "$type" } + $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } + $pf = $tagPrefix + if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" } + elseif ($t -eq "StandardPeriod") { + X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</v8:variant>" + X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>" + X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>" + X "$indent</${pf}value>" + } + elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" } + elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" } + elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" } + elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" } + else { X "$indent<${pf}value xsi:nil=`"true`"/>" } +} +function Parse-DataParamShorthand { + param([string]$s) + $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } + if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } + if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } + if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } + if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } + $s = $s.Trim() + if ($s -match '^([^=]+)=\s*(.+)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + if ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } + elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valStr } + elseif ($valStr -eq "true" -or $valStr -eq "false") { $result.value = [bool]($valStr -eq "true") } + else { $result.value = $valStr } + } else { $result.parameter = $s } + return $result +} +function Emit-DataParameters { + param($items, [string]$indent, $blockViewMode = $null) + if (-not $items -or @($items).Count -eq 0) { return } + X "$indent<dcsset:dataParameters>" + foreach ($dp in @($items)) { + if ($dp -is [string]) { + $parsed = Parse-DataParamShorthand $dp + $dpObj = New-Object PSObject + $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter + if ($null -ne $parsed.value) { $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value } + if ($parsed.use -eq $false) { $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false } + if ($parsed.userSettingID) { $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID } + if ($parsed.viewMode) { $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode } + $dp = $dpObj + } + X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">" + if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" } + X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>" + if ($dp.nilValue -eq $true) { + X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>" + } elseif ((Test-EmptyValue $dp.value) -and $dp.valueType) { + # Явный типизированный пустой (xs:string-плейсхолдер и т.п.) + Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false + } elseif (Test-EmptyValue $dp.value) { + # Нет значения и нет valueType → НЕ эмитим value-узел (form дин-список: use=false плейсхолдер). + # (В отличие от skd-settings, где значение всегда присутствует.) + } elseif ($null -ne $dp.value) { + $vtype = "$($dp.valueType)" + if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable] -or $dp.value -is [System.Collections.IDictionary]) -and ($dp.value.variant)) { + $_hasDate = $false; $_hasSD = $false + if ($dp.value -is [PSCustomObject]) { $_hasDate = [bool]$dp.value.PSObject.Properties['date']; $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] } + else { $_hasDate = $dp.value.Contains('date'); $_hasSD = $dp.value.Contains('startDate') } + $_variantStr = "$($dp.value.variant)" + $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') + if ($_isSBD) { + $_d = $null + if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } + elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } + X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">" + X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>" + if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" } + X "$indent`t`t</dcscor:value>" + } else { + $_sd = $null; $_ed = $null + if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } } + else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } } + X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">" + X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>" + if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" } + X "$indent`t`t</dcscor:value>" + } + } elseif ($vtype -match '^[a-zA-Z]+:') { + $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } + X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>" + } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { + X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>" + } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ($vtype -match '^decimal') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ($vtype -match '^string') { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } else { + X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>" + } + } + if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" } + if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" } + if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" } + X "$indent`t</dcscor:item>" + } + if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" } + X "$indent</dcsset:dataParameters>" +} + +function Emit-DLParameter { + param($p, $parsed, [string]$indent) + X "$indent<Parameter>" + $ci = "$indent`t" + X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>" + # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. + $title = $null + if ($parsed.title) { $title = $parsed.title } + elseif ($p -isnot [string] -and (Has-DLProp $p 'title') -and $p.title) { $title = $p.title } + elseif ($p -isnot [string] -and (Has-DLProp $p 'presentation') -and $p.presentation) { $title = $p.presentation } + if ($null -eq $title -or ($title -is [string] -and $title -eq '')) { $title = Title-FromName -name $parsed.name } + Emit-DLMLText -tag "dcssch:title" -text $title -indent $ci + # valueType + if ($parsed.type) { Emit-DLValueType -typeStr $parsed.type -indent $ci } + # value (дефолт nil; при valueListAllowed пустое — опускаем) + $vla = [bool]$parsed.valueListAllowed + $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) + if ($valIsArray) { + foreach ($v in @($parsed.value)) { Emit-DLValue -type $parsed.type -val $v -indent $ci -valueListAllowed $false } + } elseif ($vla -and (Test-DLEmptyValue $parsed.value) -and $parsed.valueExplicit) { + # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа здесь пишет nil + X "$ci<dcssch:value xsi:nil=`"true`"/>" + } else { + Emit-DLValue -type $parsed.type -val $parsed.value -indent $ci -valueListAllowed $vla + } + # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. + $ur = $true + if ($p -isnot [string] -and (Has-DLProp $p 'useRestriction')) { $ur = [bool]$p.useRestriction } + X "$ci<dcssch:useRestriction>$(if ($ur) { 'true' } else { 'false' })</dcssch:useRestriction>" + # expression + $expr = $null + if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" } + if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" } + # availableValues + if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) { + foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci } + } + # valueListAllowed + if ($vla) { X "$ci<dcssch:valueListAllowed>true</dcssch:valueListAllowed>" } + # availableAsField=false (hidden или явный) + $aaf = $null + if ($parsed.hidden -eq $true) { $aaf = $false } + if ($p -isnot [string] -and (Has-DLProp $p 'availableAsField')) { $aaf = [bool]$p.availableAsField } + if ($aaf -eq $false) { X "$ci<dcssch:availableAsField>false</dcssch:availableAsField>" } + # inputParameters + if ($p -isnot [string] -and (Has-DLProp $p 'inputParameters') -and $p.inputParameters) { Emit-DLInputParameters -ip $p.inputParameters -indent $ci } + # denyIncompleteValues + if ($p -isnot [string] -and (Has-DLProp $p 'denyIncompleteValues') -and $p.denyIncompleteValues -eq $true) { X "$ci<dcssch:denyIncompleteValues>true</dcssch:denyIncompleteValues>" } + # use + $useVal = $null + if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" } + if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" } + X "$indent</Parameter>" +} + +function Emit-DLParameters { + param($params, [string]$indent) + if (-not $params) { return } + foreach ($p in @($params)) { + if ($p -is [string]) { + $parsed = Parse-DLParamShorthand $p + } else { + $resolvedType = "" + if ((Has-DLProp $p 'type') -and $p.type) { + if ($p.type -is [array] -or ($p.type -is [System.Collections.IList] -and $p.type -isnot [string])) { + $resolvedType = (@($p.type | ForEach-Object { Resolve-TypeStr "$_" })) -join ' | ' + } else { $resolvedType = Resolve-TypeStr "$($p.type)" } + } elseif ((Has-DLProp $p 'valueType') -and $p.valueType) { + $resolvedType = Resolve-TypeStr "$($p.valueType)" + } + $parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); valueExplicit = (Has-DLProp $p 'value'); title = $null } + if ((Has-DLProp $p 'valueListAllowed') -and $p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } + if ((Has-DLProp $p 'hidden') -and $p.hidden -eq $true) { $parsed.hidden = $true } + } + Emit-DLParameter -p $p -parsed $parsed -indent $indent + } +} + +function Emit-Attributes { + param($attrs, [string]$indent, $conditionalAppearance = $null) + + $hasCA = $conditionalAppearance -and @($conditionalAppearance).Count -gt 0 + # Платформа ВСЕГДА эмитит <Attributes> (100% корпуса; 162 формы — пустой <Attributes/>). + if ((-not $attrs -or $attrs.Count -eq 0) -and -not $hasCA) { X "$indent<Attributes/>"; return } + if (-not $attrs -or $attrs.Count -eq 0) { + # Нет реквизитов, но есть условное оформление (последний child <Attributes>) + X "$indent<Attributes>" + Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' + X "$indent</Attributes>" + return + } + + X "$indent<Attributes>" + $seenAttrs = @{} + foreach ($attr in $attrs) { + $attrId = New-Id + $attrName = "$($attr.name)" + Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute' + + X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">" + $inner = "$indent`t`t" + + # Title атрибута (зеркало Emit-Title): нет ключа → авто-вывод из имени (кроме main); + # title "" → подавить; непустой → эмитить как есть. + $hasTitleKey = $null -ne $attr.PSObject.Properties['title'] + if ($hasTitleKey) { + if ($attr.title) { Emit-MLText -tag "Title" -text $attr.title -indent $inner } + } elseif ($attr.main -ne $true) { + Emit-MLText -tag "Title" -text (Title-FromName -name $attrName) -indent $inner + } + + # Type + if ($attr.type) { + Emit-Type -typeStr "$($attr.type)" -indent $inner + } else { + X "$inner<Type/>" + } + # valueType: уточнение типа значений ValueList → <Settings xsi:type="v8:TypeDescription"> + # (та же грамматика типа, что и Type, включая составной "A | B"). Forgiving-синонимы. + # Три состояния: нет ключа → нет Settings; "" → пустой <Settings…/>; тип → с типом. + $vtSpec = $null; $hasVt = $false + foreach ($k in @('valueType','typeDescription','описаниеТипов','типЗначений')) { + if ($attr.PSObject.Properties[$k]) { $vtSpec = $attr.$k; $hasVt = $true; break } + } + if ($hasVt) { + Emit-Type -typeStr "$vtSpec" -indent $inner -tag "Settings" -tagAttrs ' xsi:type="v8:TypeDescription"' + } + # Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика). + # Идёт сразу после <Type> (как valueType/DynamicList Settings — взаимоисключающи). + if ($attr.PSObject.Properties['planner'] -and $null -ne $attr.planner) { + Emit-PlannerSettings -pl $attr.planner -ind $inner + } + # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart">. + # Тип Settings выводится из типа реквизита (d5p1:GanttChart → d4p1:GanttChart). + if ($attr.PSObject.Properties['chart'] -and $null -ne $attr.chart) { + $ctype = if ("$($attr.type)" -match 'GanttChart') { 'd4p1:GanttChart' } else { 'd4p1:Chart' } + Emit-ChartSettings -chart $attr.chart -ind $inner -ctype $ctype + } + + if ($attr.main -eq $true) { + X "$inner<MainAttribute>true</MainAttribute>" + } + # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) + if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner } + if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner } + $mainSaved = $false + if ($attr.main -eq $true -and $attr.type) { + $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' + } + # Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто. + $emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved } + if ($emitSaved) { + X "$inner<SavedData>true</SavedData>" + } + # Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>; + # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). + # Нет ключа или false → не эмитим. + if ($null -ne $attr.PSObject.Properties['save'] -and $null -ne $attr.save) { + $saveFields = New-Object System.Collections.ArrayList + if ($attr.save -is [bool]) { + if ($attr.save) { [void]$saveFields.Add($attrName) } + } else { + foreach ($e in @($attr.save)) { + $fld = "$e" + if ([string]::IsNullOrEmpty($fld)) { continue } + if ($fld -ne $attrName -and $fld -notmatch '\.' -and $fld -notmatch '^\d+/\d+:') { $fld = "$attrName.$fld" } + if (-not $saveFields.Contains($fld)) { [void]$saveFields.Add($fld) } + } + } + if ($saveFields.Count -gt 0) { + X "$inner<Save>" + foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" } + X "$inner</Save>" + } + } + # Проверка заполнения реквизита → <FillCheck> (реальный тег; <FillChecking> в схеме нет). + # bool true → ShowError (единственное значение в корпусе); строка → verbatim. Синоним fillChecking. + $fcRaw = if ($null -ne $attr.PSObject.Properties['fillCheck']) { $attr.fillCheck } elseif ($null -ne $attr.PSObject.Properties['fillChecking']) { $attr.fillChecking } else { $null } + if ($fcRaw) { + $fcv = if ($fcRaw -is [bool]) { 'ShowError' } else { "$fcRaw" } + X "$inner<FillCheck>$fcv</FillCheck>" + } + + # UseAlways: поля, всегда читаемые (дин-список/таблица). Две формы DSL сливаются: + # attr.useAlways[] (короткие имена) + columns с useAlways:true → <Field>ИмяРеквизита.Поле</Field>. + $uaFields = New-Object System.Collections.ArrayList + if ($attr.useAlways) { + foreach ($e in @($attr.useAlways)) { + $fld = "$e" + # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" + # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. + # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). + if ($fld.StartsWith('~')) { + $bare = $fld.Substring(1) + if ($bare -notmatch "^$([regex]::Escape($attrName))\.") { $bare = "$attrName.$bare" } + $fld = "~$bare" + } elseif ($fld -notmatch "^$([regex]::Escape($attrName))\.") { + $fld = "$attrName.$fld" + } + if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } + } + } + if ($attr.columns) { + foreach ($col in $attr.columns) { + if ($col.useAlways -eq $true) { + $fld = "$attrName.$($col.name)" + if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) } + } + } + } + if ($uaFields.Count -gt 0) { + X "$inner<UseAlways>" + foreach ($f in $uaFields) { X "$inner`t<Field>$f</Field>" } + X "$inner</UseAlways>" + } + + Emit-FunctionalOptions -fo $attr.functionalOptions -indent $inner + + # Columns: прямые <Column> (ValueTable/Tree) + <AdditionalColumns table="X"> (доп. колонки + # табличных частей объекта). Порядок схемы: прямые сначала, затем AdditionalColumns-группы. + # Для дин-списка (есть settings) прямые колонки НЕ эмитим (служат лишь для UseAlways). + $hasDirectCols = $attr.columns -and $attr.columns.Count -gt 0 -and -not $attr.settings + $hasAddCols = $attr.additionalColumns -and @($attr.additionalColumns).Count -gt 0 + if ($hasDirectCols -or $hasAddCols) { + X "$inner<Columns>" + if ($hasDirectCols) { + $seenCols = @{} # колонки уникальны в пределах своего реквизита + foreach ($col in $attr.columns) { + Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'" + Emit-AttrColumn -col $col -indent "$inner`t" + } + } + if ($hasAddCols) { + foreach ($ac in @($attr.additionalColumns)) { + $acCols = @($ac.columns) + if ($acCols.Count -eq 0) { + # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) + X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>" + continue + } + X "$inner`t<AdditionalColumns table=`"$($ac.table)`">" + $seenAcCols = @{} # уникальность в пределах группы AdditionalColumns + foreach ($col in $acCols) { + Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'" + Emit-AttrColumn -col $col -indent "$inner`t`t" + } + X "$inner`t</AdditionalColumns>" + } + } + X "$inner</Columns>" + } + + # Settings (динамический список) + if ($attr.settings) { + $st = $attr.settings + X "$inner<Settings xsi:type=`"DynamicList`">" + $si = "$inner`t" + # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). + if ($null -ne $st.autoFillAvailableFields) { X "$si<AutoFillAvailableFields>$(if ($st.autoFillAvailableFields){'true'}else{'false'})</AutoFillAvailableFields>" } + $hasQuery = $st.query -and "$($st.query)".Trim() + $mq = if ($hasQuery -or $st.manualQuery -eq $true) { "true" } else { "false" } + X "$si<ManualQuery>$mq</ManualQuery>" + # DynamicDataRead: дефолт true; false только при явном отключении + $ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" } + X "$si<DynamicDataRead>$ddr</DynamicDataRead>" + if ($hasQuery) { + $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir + X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>" + } + # Явные поля набора (редко): override title/dataPath + if ($st.fields) { + foreach ($fld in $st.fields) { + # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet + # (поле-вложенный набор = реквизит табличной части; маркер nested). + $ftype = if ($fld.nested) { "DataSetFieldNestedDataSet" } else { "DataSetFieldField" } + X "$si<Field xsi:type=`"dcssch:$ftype`">" + $dp = if ($fld.dataPath) { $fld.dataPath } else { $fld.field } + X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" + X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" + if ($fld.title) { + X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">" + Emit-MLItems -val $fld.title -indent "$si`t`t" + X "$si`t</dcssch:title>" + } + X "$si</Field>" + } + } + # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. + Emit-DLParameters -params $st.parameters -indent $si + if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" } + # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). + if ($null -ne $st.autoSaveUserSettings) { X "$si<AutoSaveUserSettings>$(if ($st.autoSaveUserSettings){'true'}else{'false'})</AutoSaveUserSettings>" } + # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. + # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. + $lsi = "$si`t" + $lsOpenLen = $script:xml.Length + X "$si<ListSettings>" + $lsAfterOpenLen = $script:xml.Length # для self-closing, если внутри ничего не эмитнётся + if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) { + # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. + # meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present. + foreach ($prop in $st.listSettings.PSObject.Properties) { + $tag = $prop.Name; $meta = "$($prop.Value)" + $bvm = if ($meta -match 'v') { 'Normal' } else { $null } + switch ($tag) { + 'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } + 'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } + 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } + 'itemsViewMode' { X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" } + 'itemsUserSettingID' { X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" } + } + } + } else { + # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. + Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID + # dataParameters — после filter, до order (XSD-порядок ListSettings) + if ($st.PSObject.Properties['dataParameters']) { Emit-DataParameters -items $st.dataParameters -indent $lsi } + Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID + Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID + X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" + X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" + } + if ($script:xml.Length -eq $lsAfterOpenLen) { + # Пустой дескриптор listSettings:{} (оригинал = <ListSettings/>) → зеркалим self-closing. + $script:xml.Length = $lsOpenLen + X "$si<ListSettings/>" + } else { + X "$si</ListSettings>" + } + X "$inner</Settings>" + } + + X "$indent`t</Attribute>" + } + # Условное оформление формы — последний child <Attributes> (та же DCS-грамматика, что settings CA) + Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance' + X "$indent</Attributes>" +} + +# --- 9. Parameter emitter --- + +function Emit-Parameters { + param($params, [string]$indent) + + if (-not $params -or $params.Count -eq 0) { return } + + X "$indent<Parameters>" + $seenParams = @{} + foreach ($param in $params) { + Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter' + X "$indent`t<Parameter name=`"$($param.name)`">" + $inner = "$indent`t`t" + + Emit-Type -typeStr "$($param.type)" -indent $inner + + if ($param.key -eq $true) { + X "$inner<KeyParameter>true</KeyParameter>" + } + + X "$indent`t</Parameter>" + } + X "$indent</Parameters>" +} + +# --- 10. Command emitter --- + +function Emit-Commands { + param($cmds, [string]$indent) + + if (-not $cmds -or $cmds.Count -eq 0) { return } + + X "$indent<Commands>" + $seenCmds = @{} + foreach ($cmd in $cmds) { + $cmdId = New-Id + Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command' + X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">" + $inner = "$indent`t`t" + + # Заголовок команды (зеркало Emit-Title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс + # (в оригинале <Title> нет — не додумывать); ключ отсутствует → авто-вывод из имени (помощь модели). + if ($null -ne $cmd.PSObject.Properties['title']) { + if ($cmd.title) { Emit-MLText -tag "Title" -text $cmd.title -indent $inner } + } else { + $cmdTitle = Title-FromName -name "$($cmd.name)" + if ($cmdTitle) { Emit-MLText -tag "Title" -text $cmdTitle -indent $inner } + } + + if ($cmd.tooltip) { + Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner + } + + # Доступность команды по ролям (после ToolTip, до Action) + if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner } + + if ($cmd.action) { + X "$inner<Action>$($cmd.action)</Action>" + } + + if ($cmd.modifiesSavedData -eq $true) { X "$inner<ModifiesSavedData>true</ModifiesSavedData>" } + + Emit-FunctionalOptions -fo $cmd.functionalOptions -indent $inner + + if ($cmd.currentRowUse) { + X "$inner<CurrentRowUse>$($cmd.currentRowUse)</CurrentRowUse>" + } + + # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). + # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) + $cmdTable = $cmd.table + if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId } + if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица } + if ($cmdTable) { + X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>" + } + + if ($cmd.shortcut) { + X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" + } + + Emit-CommandPicture -pic $cmd.picture -elemLt $cmd.loadTransparent -indent $inner + + if ($cmd.representation) { + X "$inner<Representation>$($cmd.representation)</Representation>" + } + + X "$indent`t</Command>" + } + X "$indent</Commands>" +} + +# Резолв ключа-группы древовидной формы → CommandGroup (зависит от панели). Дружелюбные +# алиасы стандартных form-групп; любой иной ключ (CommandGroup.X / GUID / "") — verbatim. +function Resolve-CommandGroupKey { + param([string]$key, [string]$panelTag) + $k = ($key -replace '\s','').ToLower() + if ($panelTag -eq 'NavigationPanel') { + switch ($k) { + 'important' { return 'FormNavigationPanelImportant' } + 'важное' { return 'FormNavigationPanelImportant' } + 'goto' { return 'FormNavigationPanelGoTo' } + 'перейти' { return 'FormNavigationPanelGoTo' } + 'seealso' { return 'FormNavigationPanelSeeAlso' } + 'смтакже' { return 'FormNavigationPanelSeeAlso' } + } + } else { + switch ($k) { + 'important' { return 'FormCommandBarImportant' } + 'важное' { return 'FormCommandBarImportant' } + 'createbasedon' { return 'FormCommandBarCreateBasedOn' } + 'создатьнаосновании' { return 'FormCommandBarCreateBasedOn' } + } + } + return $key # verbatim +} + +# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. +# Значение панели: МАССИВ (плоская форма; элемент может нести group) ИЛИ ОБЪЕКТ +# (древовидная форма: {группа: [команды]}, group берётся из ключа, элементы его не дублируют). +# Элемент: строка (голый command, Type=Auto) или объект. Порядок тегов: +# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). +function Emit-CommandInterface { + param($ci, [string]$indent) + if (-not $ci) { return } + $inner = "$indent`t" + $panels = @( + @{ Tag='CommandBar'; Syns=@('commandBar','команднаяПанель','КоманднаяПанель') }, + @{ Tag='NavigationPanel'; Syns=@('navigationPanel','панельНавигации','ПанельНавигации') } + ) + $present = @() + foreach ($p in $panels) { + $items = $null + foreach ($syn in $p.Syns) { if ($null -ne $ci.PSObject.Properties[$syn]) { $items = $ci.($syn); break } } + if ($null -ne $items) { $present += ,@{ Tag=$p.Tag; Items=$items } } + } + if ($present.Count -eq 0) { return } + X "$indent<CommandInterface>" + foreach ($p in $present) { + X "$inner<$($p.Tag)>" + # Нормализация: плоский список пар (элемент, group-из-дерева). Объект → дерево. + $flat = New-Object System.Collections.ArrayList + if ($p.Items -is [System.Management.Automation.PSCustomObject]) { + foreach ($prop in $p.Items.PSObject.Properties) { + $grpFromTree = Resolve-CommandGroupKey -key $prop.Name -panelTag $p.Tag + foreach ($it in @($prop.Value)) { [void]$flat.Add(@{ item=$it; treeGroup=$grpFromTree }) } + } + } else { + foreach ($it in @($p.Items)) { [void]$flat.Add(@{ item=$it; treeGroup=$null }) } + } + foreach ($fi in $flat) { + $item = $fi.item; $treeGroup = $fi.treeGroup + if ($item -is [string]) { + $cmd = $item; $type = 'Auto'; $attr = $null; $grp = $null; $idx = $null; $dv = $null; $vis = $null + } else { + $cmd = Get-ElProp $item @('command','команда') + $type = Get-ElProp $item @('type','тип'); if (-not $type) { $type = 'Auto' } + $attr = Get-ElProp $item @('attribute','реквизит') + $grp = Get-ElProp $item @('group','группа','группаКоманд') + $idx = Get-ElProp $item @('index','индекс') + $dv = Get-ElProp $item @('defaultVisible','видимость','видимостьПоУмолчанию') + $vis = Get-ElProp $item @('visible','видимостьПоРолям','настройкаВидимости') + } + # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк + if ($treeGroup) { $grp = $treeGroup } + X "$inner`t<Item>" + X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>" + X "$inner`t`t<Type>$type</Type>" + if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" } + if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" } + if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" } + if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" } + if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" } + X "$inner`t</Item>" + } + X "$inner</$($p.Tag)>" + } + X "$indent</CommandInterface>" +} + +# --- 11. Properties emitter --- + +function Emit-Properties { + param($props, [string]$indent) + + if (-not $props) { return } + + # camelCase -> PascalCase mapping for known properties + $propMap = @{ + "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" + } + + foreach ($p in $props.PSObject.Properties) { + $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { + # Auto PascalCase: first letter uppercase + $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) + } + # Convert boolean to lowercase string (PS renders as True/False) + $val = $p.Value + # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) + if ($val -is [string] -and $val -eq '') { continue } + if ($val -is [bool]) { + $val = if ($val) { "true" } else { "false" } + } + X "$indent<$xmlName>$val</$xmlName>" + } +} + +# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- + +# Companion-панели как СВОЙСТВА элемента (значение объект/массив): любой знакомый +# синоним → каноника commandBar / contextMenu. Разводим с одноимёнными ТИПАМИ-элементами +# по типу значения: строка = элемент-тип (имя), объект/массив = панель-свойство. +function Normalize-PanelSynonyms { + param($el) + if ($null -eq $el) { return } + $panelSyns = @{ + 'commandBar' = @('commandBar','autoCommandBar','AutoCommandBar','autoCmdBar','cmdBar','КоманднаяПанель') + 'contextMenu' = @('contextMenu','ContextMenu','КонтекстноеМеню') + } + foreach ($canon in $panelSyns.Keys) { + foreach ($syn in $panelSyns[$canon]) { + $p = $el.PSObject.Properties[$syn] + if ($null -ne $p -and ($p.Value -is [array] -or $p.Value -is [System.Management.Automation.PSCustomObject])) { + if ($syn -ne $canon -and $null -eq $el.PSObject.Properties[$canon]) { + $v = $p.Value + $el.PSObject.Properties.Remove($syn) | Out-Null + $el | Add-Member -NotePropertyName $canon -NotePropertyValue $v -Force + } + break + } + } + } +} + +function Normalize-ElementSynonyms { + param($el) + if ($null -eq $el) { return } + Normalize-PanelSynonyms $el + # Тип-синонимы (commandBar/autoCommandBar → элемент-тип) применяем ТОЛЬКО к строковому + # значению (имя элемента); объект/массив уже отнесён к панель-свойству выше. + $typeSyn = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" } + foreach ($pair in $typeSyn.GetEnumerator()) { + $src = $el.PSObject.Properties[$pair.Key] + if ($null -ne $src -and ($src.Value -is [string]) -and $null -eq $el.PSObject.Properties[$pair.Value]) { + $val = $el.($pair.Key) + $el.PSObject.Properties.Remove($pair.Key) | Out-Null + $el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force + } + } + if ($el.PSObject.Properties["extTooltip"] -and $null -eq $el.PSObject.Properties["extendedTooltip"]) { + $val = $el.extTooltip + $el.PSObject.Properties.Remove("extTooltip") | Out-Null + $el | Add-Member -NotePropertyName "extendedTooltip" -NotePropertyValue $val -Force + } + # Рекурсия в детей панелей (commandBar/contextMenu) — нормализуем кнопки/группы внутри + foreach ($pk in @('commandBar','contextMenu')) { + $pp = $el.PSObject.Properties[$pk] + if ($null -ne $pp) { + $kids = if ($pp.Value -is [array]) { $pp.Value } elseif ($null -ne $pp.Value) { $pp.Value.children } else { $null } + if ($kids) { foreach ($child in $kids) { Normalize-ElementSynonyms $child } } + } + } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { Normalize-ElementSynonyms $child } + } + if ($el.PSObject.Properties["columns"] -and $el.columns) { + foreach ($child in $el.columns) { Normalize-ElementSynonyms $child } + } +} + +function HasCmdBarRecursive { + param($el) + if ($null -eq $el) { return $false } + if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } } + } + if ($el.PSObject.Properties["columns"] -and $el.columns) { + foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } } + } + return $false +} + +function ApplyDynamicListTableHeuristic { + param($el, [string]$listName, [bool]$hasMainTable) + if ($null -eq $el) { return } + if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) { + # Маркер дин-список-таблицы → Emit-Table эмитит блок свойств (Group A defaults) + $el | Add-Member -NotePropertyName "_dynList" -NotePropertyValue $true -Force + if ($null -eq $el.PSObject.Properties["tableAutofill"]) { + $el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force + } + if ($null -eq $el.PSObject.Properties["commandBarLocation"]) { + $el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force + } + # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. + # Декомпилятор опускает ключ при rpdp == smart-default (ждёт реинъекции); реальное отсутствие + # фиксирует ""-маркером (НЕ перезатирается). Гейт hasMainTable снят: дин-список без mainTable + # (напр. query-based) тоже несёт RowPictureDataPath. + if ($null -eq $el.PSObject.Properties["rowPictureDataPath"]) { + $el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force + } + } + if ($el.PSObject.Properties["children"] -and $el.children) { + foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable } + } +} + +function Test-IsObjectLikeType { + param([string]$type) + if ([string]::IsNullOrEmpty($type)) { return $false } + if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true } + $objectSuffixes = @( + "CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject", + "ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject", + "TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject", + "ChartOfCalculationTypesObject", "ExchangePlanObject" + ) + $recordSetPrefixes = @( + "InformationRegisterRecordSet", "AccumulationRegisterRecordSet", + "AccountingRegisterRecordSet", "CalculationRegisterRecordSet", + "InformationRegisterRecordManager" + ) + foreach ($suffix in $objectSuffixes) { + if ($type -like "$suffix.*") { return $true } + } + foreach ($prefix in $recordSetPrefixes) { + if ($type -like "$prefix.*") { return $true } + } + return $false +} + +# 11b.1: Normalize synonyms recursively +if ($def.elements) { + foreach ($el in $def.elements) { Normalize-ElementSynonyms $el } +} + +# 11b.2: Extract autoCmdBar element from def.elements +$script:mainAcbDef = $null +if ($def.elements) { + $autoBars = @() + $rest = @() + foreach ($el in $def.elements) { + if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) { + $autoBars += $el + } else { + $rest += $el + } + } + if ($autoBars.Count -gt 1) { + Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed." + exit 1 + } + if ($autoBars.Count -eq 1) { + $script:mainAcbDef = $autoBars[0] + # Replace def.elements with the filtered list + $def.PSObject.Properties.Remove("elements") | Out-Null + $def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force + } +} + +# 11b.3: Infer main attribute (only if no attribute has main:true) +if ($def.attributes) { + $hasExplicitMain = $false + foreach ($attr in $def.attributes) { + if ($attr.main -eq $true) { $hasExplicitMain = $true; break } + } + if (-not $hasExplicitMain) { + $candidates = @() + foreach ($attr in $def.attributes) { + # Skip if user explicitly opted out via main:false + if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue } + if (Test-IsObjectLikeType "$($attr.type)") { + $candidates += $attr + } + } + if ($candidates.Count -eq 1) { + $candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force + Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))" + } elseif ($candidates.Count -gt 1) { + $names = ($candidates | ForEach-Object { $_.name }) -join ", " + Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly" + } + } +} + +# 11b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) +if ($def.attributes -and $def.elements) { + foreach ($attr in $def.attributes) { + if ("$($attr.type)" -ne "DynamicList") { continue } + $mt = $null + if ($attr.PSObject.Properties["settings"] -and $null -ne $attr.settings) { + if ($attr.settings -is [hashtable]) { + if ($attr.settings.ContainsKey("mainTable")) { $mt = $attr.settings["mainTable"] } + } elseif ($attr.settings.PSObject.Properties["mainTable"]) { + $mt = $attr.settings.mainTable + } + } + $hasMt = -not [string]::IsNullOrEmpty("$mt") + foreach ($el in $def.elements) { + ApplyDynamicListTableHeuristic $el $attr.name $hasMt + } + } +} + +# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3 +function Compute-MainAcbAutofill { + if ($script:mainAcbDef) { + if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) { + return [bool]$script:mainAcbDef.autofill + } + return $true + } + if ($def.elements) { + foreach ($el in $def.elements) { + if (HasCmdBarRecursive $el) { return $false } + } + } + return $true +} + +# --- 12. Main compilation --- + +# Title +if ($def.title) { + Emit-MLText -tag "Title" -text $def.title -indent "`t" +} + +# Header +X '<?xml version="1.0" encoding="UTF-8"?>' +X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" + +# Oops — Title was emitted before header. Need to fix the order. +# Actually, let me restructure: build the body into a separate buffer, then assemble + +# Reset and rebuild properly +$script:xml = New-Object System.Text.StringBuilder 8192 +$script:nextId = 1 +$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме) + +X '<?xml version="1.0" encoding="UTF-8"?>' +X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">" + +# 12a. Title (from def.title or properties.title — must be multilingual XML) +$formTitle = $def.title +if (-not $formTitle -and $def.properties -and $def.properties.title) { + $formTitle = $def.properties.title +} +if ($formTitle) { + Emit-MLText -tag "Title" -text $formTitle -indent "`t" +} + +# 12b. Properties (skip 'title' — handled above as multilingual) +# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; +# otherwise platform appends synonym → "Title: Synonym" double-titles). +$propsClone = New-Object PSObject +$hasAutoTitle = $false +if ($def.properties) { + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true } + } +} +if ($formTitle -and -not $hasAutoTitle) { + $propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false +} +if ($def.properties) { + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -ne "title") { + $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value + } + } +} +Emit-Properties -props $propsClone -indent "`t" + +# 12c. CommandSet (excluded commands) +if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { + X "`t<CommandSet>" + foreach ($cmd in $def.excludedCommands) { + X "`t`t<ExcludedCommand>$cmd</ExcludedCommand>" + } + X "`t</CommandSet>" +} + +# 12c2. MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок +# (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). +if ($def.mobileCommandBarContent -and @($def.mobileCommandBarContent).Count -gt 0) { + X "`t<MobileDeviceCommandBarContent>" + foreach ($nm in @($def.mobileCommandBarContent)) { + X "`t`t<xr:Item>" + X "`t`t`t<xr:Presentation/>" + X "`t`t`t<xr:CheckState>0</xr:CheckState>" + X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" + X "`t`t</xr:Item>" + } + X "`t</MobileDeviceCommandBarContent>" +} + +# 12d. AutoCommandBar (always present, id=-1) +$acbAutofill = Compute-MainAcbAutofill +$acbName = "ФормаКоманднаяПанель" +$acbHAlign = $null +if ($script:mainAcbDef) { + if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") { + $acbName = "$($script:mainAcbDef.autoCmdBar)" + } + if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") { + $acbName = "$($script:mainAcbDef.name)" + } + if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") { + $acbHAlign = "$($script:mainAcbDef.horizontalAlign)" + } +} +$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0) +$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren) +if ($acbHasInner) { + X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">" + if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" } + if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" } + if ($hasAcbChildren) { + X "`t`t<ChildItems>" + foreach ($child in $script:mainAcbDef.children) { + Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true + } + X "`t`t</ChildItems>" + } + X "`t</AutoCommandBar>" +} else { + X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"/>" +} + +# 12e. Events +if ($def.events) { + foreach ($p in $def.events.PSObject.Properties) { + if ($script:knownFormEvents -notcontains $p.Name) { + Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" + } + } + X "`t<Events>" + foreach ($p in $def.events.PSObject.Properties) { + X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>" + } + X "`t</Events>" +} + +# 12f. ChildItems (elements) +if ($def.elements -and $def.elements.Count -gt 0) { + X "`t<ChildItems>" + foreach ($el in $def.elements) { + Emit-Element -el $el -indent "`t`t" + } + X "`t</ChildItems>" +} + +# 12g. Attributes +Emit-Attributes -attrs $def.attributes -indent "`t" -conditionalAppearance $def.conditionalAppearance + +# 12h. Parameters +Emit-Parameters -params $def.parameters -indent "`t" + +# 12i. Commands +Emit-Commands -cmds $def.commands -indent "`t" + +# 12i2. CommandInterface (командный интерфейс формы — последний дочерний Form) +Emit-CommandInterface -ci $def.commandInterface -indent "`t" + +# 12j. Close +X '</Form>' + +# --- 13. Write output --- + +$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } +$outDir = [System.IO.Path]::GetDirectoryName($outPath) +if (-not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) + +# --- 13b. Auto-register form in parent object XML --- + +# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml +$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath) +$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir) +$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir) +$objectDir = [System.IO.Path]::GetDirectoryName($formsDir) +$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir) + +$formName = [System.IO.Path]::GetFileName($formNameDir) +$objectName = [System.IO.Path]::GetFileName($objectDir) +$formsLeaf = [System.IO.Path]::GetFileName($formsDir) + +if ($formsLeaf -eq 'Forms') { + $objectXmlPath = Join-Path $typePluralDir "$objectName.xml" + if (Test-Path $objectXmlPath) { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objectXmlPath) + + $nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr) + if ($childObjects) { + $existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr) + if (-not $existing) { + $formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formElem.InnerText = $formName + + $insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr) + if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) } + + if ($insertBefore) { + $childObjects.InsertBefore($formElem, $insertBefore) | Out-Null + $ws = $objDoc.CreateWhitespace("`n`t`t`t") + $childObjects.InsertBefore($ws, $insertBefore) | Out-Null + } else { + $lastChild = $childObjects.LastChild + if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { + $childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null + $childObjects.InsertBefore($formElem, $lastChild) | Out-Null + } else { + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null + $childObjects.AppendChild($formElem) | Out-Null + $childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null + } + } + + $regEnc = New-Object System.Text.UTF8Encoding($true) + $regSettings = New-Object System.Xml.XmlWriterSettings + $regSettings.Encoding = $regEnc + $regSettings.Indent = $false + $regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create) + $regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings) + $objDoc.Save($regWriter) + $regWriter.Close() + $regStream.Close() + + Write-Host " Registered: <Form>$formName</Form> in $objectName.xml" + } + } + } +} + +# --- 14. Summary --- + +$elCount = $script:nextId - 1 +Write-Host "[OK] Compiled: $OutputPath" +Write-Host " Elements+IDs: $elCount" +if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } +if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } +if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 6e7cc287..ab582577 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,6047 +1,6062 @@ -#!/usr/bin/env python3 -# form-compile v1.137 — 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_fields(parent_node, tag_name='Attribute'): - """Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag).""" - result = [] - if parent_node is None: - return result - for field_node in _et_findall(parent_node, f'md:{tag_name}'): - fp = _et_find(field_node, 'md:Properties') - f_name = _et_text(fp, 'md:Name') - f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") - f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name - f_type_node = _et_find(fp, 'md:Type') - f_type = extract_type(f_type_node) - result.append({ - 'Name': f_name, - 'Synonym': f_syn, - 'Type': f_type, - 'IsRef': is_ref_type(f_type), - }) - return result - - # Attributes - attributes = extract_fields(child_objs, 'Attribute') - - # 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_fields(ts_co, 'Attribute') - 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 - elif obj_type == 'InformationRegister': - meta['Dimensions'] = extract_fields(child_objs, 'Dimension') - meta['Resources'] = extract_fields(child_objs, 'Resource') - prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity') - meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical' - wm_node = _et_find(props_node, 'md:WriteMode') - meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent' - elif obj_type == 'AccumulationRegister': - meta['Dimensions'] = extract_fields(child_objs, 'Dimension') - meta['Resources'] = extract_fields(child_objs, 'Resource') - rt_node = _et_find(props_node, 'md:RegisterType') - meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances' - elif obj_type == 'ChartOfCharacteristicTypes': - 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 - meta['HasValueType'] = True - elif obj_type == 'ExchangePlan': - 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 - meta['Hierarchical'] = False - meta['HierarchyType'] = None - meta['Owners'] = [] - elif obj_type == 'ChartOfAccounts': - 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 - meta['Hierarchical'] = 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' - meta['Owners'] = [] - max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount') - meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0 - meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag') - meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag') - - 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'}, - }, - # --- Register defaults --- - 'informationRegister.record': { - 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - 'informationRegister.list': { - 'columns': 'all', 'columnType': 'labelField', - 'tableCommandBar': 'none', 'commandBar': 'auto', - 'properties': {}, - }, - 'accumulationRegister.list': { - 'columns': 'all', 'columnType': 'labelField', - 'tableCommandBar': 'none', 'commandBar': 'auto', - 'properties': {}, - }, - # --- Catalog-like type defaults --- - 'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'}, - 'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'}, - 'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'}, - 'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'}, - 'exchangePlan.item': {'basedOn': 'catalog.item'}, - 'exchangePlan.list': {'basedOn': 'catalog.list'}, - 'exchangePlan.choice': {'basedOn': 'catalog.choice'}, - # --- ChartOfAccounts defaults --- - 'chartOfAccounts.item': { - 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, - 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, - 'properties': {}, - }, - 'chartOfAccounts.folder': { - 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, - 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, - }, - 'chartOfAccounts.list': {'basedOn': 'catalog.list'}, - 'chartOfAccounts.choice': {'basedOn': 'catalog.choice'}, - } - - # 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 - - -# Non-displayable types — cannot be bound to form elements -NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения') - -def is_displayable_type(type_str): - return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES) - -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 - - # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. - # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы - # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) - - # 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']: - if not is_displayable_type(attr['Type']): - continue - 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'), ('userVisible', 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 - if not is_displayable_type(attr['Type']): - 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 = [] - # Standard columns: Number + Date - columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) - columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) - # All custom attributes as labelField - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - 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'), ('userVisible', 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), - ]) - - 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 and is_displayable_type(attr['Type'])] - - # 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)]) - ]), - ]) - - -# --- InformationRegister DSL generators --- - -def generate_information_register_dsl(meta, preset_data, purpose): - p_key = f"informationRegister.{purpose.lower()}" - p = preset_data.get(p_key, {}) - fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} - dispatch = { - 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), - 'List': lambda: generate_information_register_list_dsl(meta, p), - } - return dispatch[purpose]() - - -def generate_information_register_record_dsl(meta, p, fd): - elements = OrderedDict() - is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' - - # Period first (if periodic) - if is_periodic: - elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_information_register_list_dsl(meta, p): - is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' - is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' - - columns_list = [] - # Period - if is_periodic: - columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) - # Recorder/LineNumber for subordinate registers - if is_recorder_subordinate: - columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) - columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - - 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_list), - ]) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', [table_el]), - ('attributes', [ - {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} - ]), - ]) - - -# --- AccumulationRegister DSL generators --- - -def generate_accumulation_register_dsl(meta, preset_data, purpose): - p_key = f"accumulationRegister.{purpose.lower()}" - p = preset_data.get(p_key, {}) - dispatch = { - 'List': lambda: generate_accumulation_register_list_dsl(meta, p), - } - return dispatch[purpose]() - - -def generate_accumulation_register_list_dsl(meta, p): - columns_list = [] - # AccumulationRegisters always have Period, Recorder, LineNumber - columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) - columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) - columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) - # Dimensions - for dim in meta.get('Dimensions', []): - if not is_displayable_type(dim['Type']): - continue - columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) - # Resources - for res in meta.get('Resources', []): - if not is_displayable_type(res['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) - # Attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' - columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) - - 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_list), - ]) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', [table_el]), - ('attributes', [ - {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} - ]), - ]) - - -# --- ChartOfCharacteristicTypes (delegates to Catalog) --- - -def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): - # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. - dsl = generate_catalog_dsl(meta, preset_data, purpose) - - # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types - cat_obj_type = f"CatalogObject.{meta['Name']}" - ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" - cat_list_type = f"Catalog.{meta['Name']}" - ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" - - for a in dsl['attributes']: - if a.get('type') == cat_obj_type: - a['type'] = ccoct_obj_type - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: - a['settings']['mainTable'] = ccoct_list_type - - # For Item forms: inject ValueType field after Description/ГруппаКодНаименование - if purpose == 'Item' and dsl.get('elements'): - vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) - els = dsl['elements'] - if isinstance(els, list): - inserted = False - new_els = [] - for el in els: - new_els.append(el) - if not inserted and isinstance(el, dict): - name = el.get('input') or el.get('group') or '' - if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): - new_els.append(vt_el) - inserted = True - if not inserted: - new_els.append(vt_el) - dsl['elements'] = new_els - - return dsl - - -# --- ExchangePlan (delegates to Catalog) --- - -def generate_exchange_plan_dsl(meta, preset_data, purpose): - # ExchangePlans are not hierarchical and have no Folder form - dsl = generate_catalog_dsl(meta, preset_data, purpose) - - # Post-patch: replace Catalog types with ExchangePlan types - cat_obj_type = f"CatalogObject.{meta['Name']}" - ep_obj_type = f"ExchangePlanObject.{meta['Name']}" - cat_list_type = f"Catalog.{meta['Name']}" - ep_list_type = f"ExchangePlan.{meta['Name']}" - - for a in dsl['attributes']: - if a.get('type') == cat_obj_type: - a['type'] = ep_obj_type - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: - a['settings']['mainTable'] = ep_list_type - - # For Item forms: inject SentNo, ReceivedNo after Code/Description - if purpose == 'Item' and dsl.get('elements'): - sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) - recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) - els = dsl['elements'] - if isinstance(els, list): - inserted = False - new_els = [] - for el in els: - new_els.append(el) - if not inserted and isinstance(el, dict): - name = el.get('input') or el.get('group') or '' - if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): - new_els.append(sent_el) - new_els.append(recv_el) - inserted = True - if not inserted: - new_els.append(sent_el) - new_els.append(recv_el) - dsl['elements'] = new_els - - return dsl - - -# --- ChartOfAccounts DSL generators --- - -def generate_chart_of_accounts_dsl(meta, preset_data, purpose): - p_key = f"chartOfAccounts.{purpose.lower()}" - p = preset_data.get(p_key, {}) - fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} - dispatch = { - 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), - 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), - 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), - 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), - } - return dispatch[purpose]() - - -def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): - elements = [] - - # Header: Code + Parent - header_left_children = [] - if meta.get('CodeLength', 0) > 0: - header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - header_right_children = [] - if meta.get('Hierarchical'): - parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') - header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) - - if len(header_right_children) > 0: - elements.append(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', header_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', header_right_children)]), - ]), - ])) - elif len(header_left_children) > 0: - elements.extend(header_left_children) - - # Description - if meta.get('DescriptionLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - - # OffBalance - elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) - - # AccountingFlags as checkboxes - if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: - flag_children = [] - for flag in meta['AccountingFlags']: - flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) - elements.append(OrderedDict([ - ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), - ('children', flag_children), - ])) - - # ExtDimensionTypes table - if meta.get('MaxExtDimensionCount', 0) > 0: - # Column names are prefixed with the table name (like the generic TS path and stock 1C), - # else a subconto flag column collides with a same-named account accounting-flag checkbox. - ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e' - ed_cols = [] - ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) - ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) - if meta.get('ExtDimensionAccountingFlags'): - for ed_flag in meta['ExtDimensionAccountingFlags']: - ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) - elements.append(OrderedDict([ - ('table', ed_table), - ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), - ('columns', ed_cols), - ])) - - # Custom attributes - for attr in meta['Attributes']: - if not is_displayable_type(attr['Type']): - continue - elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) - - # 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'] - for ts in meta['TabularSections']: - if ts['Name'] in ts_exclude: - continue - ts_cols = [] - for col in ts['Columns']: - if not is_displayable_type(col['Type']): - continue - 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)) - elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) - - props = OrderedDict() - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_chart_of_accounts_folder_dsl(meta, p): - elements = [] - if meta.get('CodeLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) - if meta.get('DescriptionLength', 0) > 0: - elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) - if meta.get('Hierarchical'): - parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') - elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) - - props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) - if p.get('properties'): - for k in p['properties']: - props[k] = p['properties'][k] - - return OrderedDict([ - ('title', meta['Synonym']), - ('useForFoldersAndItems', 'Folders'), - ('properties', props), - ('elements', elements), - ('attributes', [ - {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} - ]), - ]) - - -def generate_chart_of_accounts_list_dsl(meta, preset_data): - # Delegate to Catalog List and patch types - dsl = generate_catalog_dsl(meta, preset_data, 'List') - for a in dsl['attributes']: - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": - a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" - return dsl - - -def generate_chart_of_accounts_choice_dsl(meta, preset_data): - dsl = generate_catalog_dsl(meta, preset_data, 'Choice') - for a in dsl['attributes']: - if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": - a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" - return dsl - - -# ═══════════════════════════════════════════════════════════════════════════ -# END OF FROM-OBJECT MODE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════ - - -def esc_xml(s): - # Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > . - # Кавычки/апострофы в тексте 1С не экранирует (пишет литерально) — " ломал бы раундтрип. - return s.replace('&', '&').replace('<', '<').replace('>', '>') - - -def di_attr(el): - # DisplayImportance — атрибут открывающего тега элемента (адаптивная важность). "" если нет. - if isinstance(el, dict) and el.get('displayImportance'): - return f' DisplayImportance="{esc_xml(str(el["displayImportance"]))}"' - return '' - - -# Базовая директория для @file-ссылок в query динсписка (устанавливается в main) -QUERY_BASE_DIR = None - - -def resolve_query_value(val, base_dir): - if not val.startswith('@'): - return val - file_path = val[1:] - if os.path.isabs(file_path): - candidates = [file_path] - else: - candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)] - for c in candidates: - if os.path.exists(c): - with open(c, 'r', encoding='utf-8-sig') as f: - return f.read().rstrip() - print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) - sys.exit(1) - - -def emit_ml_items(lines, indent, val): - # строка → один ru-элемент; объект {lang: text} → по элементу на язык - if isinstance(val, dict): - for k, v in val.items(): - lines.append(f"{indent}<v8:item>") - lines.append(f"{indent}\t<v8:lang>{k}</v8:lang>") - lines.append(f"{indent}\t<v8:content>{esc_xml(str(v))}</v8:content>") - lines.append(f"{indent}</v8:item>") - else: - lines.append(f"{indent}<v8:item>") - lines.append(f"{indent}\t<v8:lang>ru</v8:lang>") - lines.append(f"{indent}\t<v8:content>{esc_xml(str(val))}</v8:content>") - lines.append(f"{indent}</v8:item>") - - -def emit_mltext(lines, indent, tag, text, xsi_type=None): - attr = f' xsi:type="{xsi_type}"' if xsi_type else '' - if not text: - lines.append(f"{indent}<{tag}{attr}/>") - return - lines.append(f"{indent}<{tag}{attr}>") - emit_ml_items(lines, f"{indent}\t", text) - lines.append(f"{indent}</{tag}>") - - -def emit_us_presentation(lines, indent, tag, val): - # <dcsset:userSettingPresentation>: плоская строка → xsi:type="xs:string"; мультиязычный → v8:LocalStringType - if val is None: - return - if isinstance(val, str): - lines.append(f'{indent}<{tag} xsi:type="xs:string">{esc_xml(val)}</{tag}>') - else: - emit_mltext(lines, indent, tag, val, xsi_type='v8:LocalStringType') - - -# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). -CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' -CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' -CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' -CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' - - -def new_uuid(): - return str(uuid.uuid4()) - - -# ───────────────────────────────────────────────────────────────────────────── -# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. -# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). -# ───────────────────────────────────────────────────────────────────────────── -COMPARISON_TYPES = { - '=': 'Equal', '<>': 'NotEqual', - '>': 'Greater', '>=': 'GreaterOrEqual', - '<': 'Less', '<=': 'LessOrEqual', - 'in': 'InList', 'notIn': 'NotInList', - 'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy', - 'contains': 'Contains', 'notContains': 'NotContains', - 'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith', - 'like': 'Like', 'notLike': 'NotLike', - 'подобно': 'Like', 'неподобно': 'NotLike', # рус. синоним - 'filled': 'Filled', 'notFilled': 'NotFilled', -} -# Регистронезависимый лукап (зеркало PS-хэша): Like/LIKE/ПОДОБНО → канон -_COMPARISON_TYPES_CI = {k.lower(): v for k, v in COMPARISON_TYPES.items()} - -_REF_TYPE_RE = re.compile( - r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|' - r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|' - r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|' - r'InformationRegister|ExchangePlan)\.') - - -def parse_filter_shorthand(s): - result = {'field': '', 'op': 'Equal', 'value': None, 'use': True, - 'userSettingID': None, 'viewMode': None, 'presentation': None} - if re.search(r'@user', s): - result['userSettingID'] = 'auto' - s = re.sub(r'\s*@user', '', s) - if re.search(r'@off', s): - result['use'] = False - s = re.sub(r'\s*@off', '', s) - if re.search(r'@quickAccess', s): - result['viewMode'] = 'QuickAccess' - s = re.sub(r'\s*@quickAccess', '', s) - if re.search(r'@normal', s): - result['viewMode'] = 'Normal' - s = re.sub(r'\s*@normal', '', s) - if re.search(r'@inaccessible', s): - result['viewMode'] = 'Inaccessible' - s = re.sub(r'\s*@inaccessible', '', s) - s = s.strip() - op_patterns = ['<>', '>=', '<=', '=', '>', '<', - r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b', - r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b', - r'notLike\b', r'like\b', r'неподобно\b', r'подобно\b', - r'notFilled\b', r'filled\b'] - op_joined = '|'.join(op_patterns) - m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s, re.IGNORECASE) - if m: - result['field'] = m.group(1).strip() - result['op'] = m.group(2).strip() - val_part = m.group(3).strip() if m.group(3) else '' - if val_part and val_part != '_': - if val_part == 'true' or val_part == 'false': - result['value'] = (val_part == 'true') - result['valueType'] = 'xs:boolean' - elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part): - # дата без valueType → emit_filter_item выведет StandardBeginningDate Custom (дефолт даты в фильтре) - result['value'] = val_part - elif re.match(r'^\d+(\.\d+)?$', val_part): - result['value'] = val_part - result['valueType'] = 'xs:decimal' - elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part): - result['value'] = val_part - result['valueType'] = 'dcscor:DesignTimeValue' - else: - result['value'] = val_part - result['valueType'] = 'xs:string' - else: - result['field'] = s - return result - - -def _value_type_for(v, explicit=None): - if explicit: - return explicit - if isinstance(v, bool): - return 'xs:boolean' - if isinstance(v, (int, float)): - return 'xs:decimal' - vs = str(v) - if re.match(r'^\d{4}-\d{2}-\d{2}T', vs): - return 'xs:dateTime' - if re.match(r'^-?\d+(\.\d+)?$', vs): - return 'xs:decimal' - if _REF_TYPE_RE.match(vs): - return 'dcscor:DesignTimeValue' - return 'xs:string' - - -def emit_filter_item(lines, item, indent): - if item.get('group'): - g = str(item['group']) - group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group') - lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemGroup">') - lines.append(f'{indent}\t<dcsset:groupType>{group_type}</dcsset:groupType>') - if item.get('items'): - for sub in item['items']: - if isinstance(sub, str): - parsed = parse_filter_shorthand(sub) - obj = {'field': parsed['field'], 'op': parsed['op']} - if parsed['use'] is False: - obj['use'] = False - if parsed['value'] is not None: - obj['value'] = parsed['value'] - if parsed.get('valueType'): - obj['valueType'] = parsed['valueType'] - if parsed.get('userSettingID'): - obj['userSettingID'] = parsed['userSettingID'] - if parsed.get('viewMode'): - obj['viewMode'] = parsed['viewMode'] - sub = obj - emit_filter_item(lines, sub, f'{indent}\t') - if item.get('presentation'): - emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) - if item.get('viewMode'): - lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') - if item.get('userSettingID'): - guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) - lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(guid)}</dcsset:userSettingID>') - if item.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) - lines.append(f'{indent}</dcsset:item>') - return - - lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">') - if item.get('use') is False: - lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>') - lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(str(item.get("field", "")))}</dcsset:left>') - # Регистронезависимый лукап (зеркало PS): Like/LIKE/ПОДОБНО → канон; иначе — как есть - comp_type = _COMPARISON_TYPES_CI.get(str(item.get('op')).lower()) - if not comp_type: - comp_type = str(item.get('op')) - lines.append(f'{indent}\t<dcsset:comparisonType>{esc_xml(comp_type)}</dcsset:comparisonType>') - val = item.get('value') - if isinstance(val, list): - if len(val) == 0: - lines.append(f'{indent}\t<dcsset:right xsi:type="v8:ValueListType">') - lines.append(f'{indent}\t\t<v8:valueType/>') - lines.append(f'{indent}\t\t<v8:lastId xsi:type="xs:decimal">-1</v8:lastId>') - lines.append(f'{indent}\t</dcsset:right>') - else: - for v in val: - vt = _value_type_for(v, item.get('valueType')) - v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v)) - lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>') - elif val is not None and ( - re.search(r'Standard(Beginning|End)Date$', str(item.get('valueType') or '')) or - (not item.get('valueType') and isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val))): - # Стандартная дата начала/окончания. Формы: объект {variant, date?} (Custom несёт <v8:date>); - # строка-вариант "BeginningOfThisDay" (именованный без даты); голая ISO-дата без valueType — - # шорткат для Custom+date (дата в фильтре почти всегда SBD Custom, корпус 268 vs 2 xs:dateTime). - sd_type = re.sub(r'^v8:', '', str(item['valueType'])) if item.get('valueType') else 'StandardBeginningDate' - if isinstance(val, dict): - variant = str(val.get('variant', '')); date_v = val.get('date') - elif isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val): - variant = 'Custom'; date_v = val - else: - variant = str(val); date_v = None - lines.append(f'{indent}\t<dcsset:right xsi:type="v8:{sd_type}">') - lines.append(f'{indent}\t\t<v8:variant xsi:type="v8:{sd_type}Variant">{esc_xml(variant)}</v8:variant>') - if date_v is not None: - lines.append(f'{indent}\t\t<v8:date>{esc_xml(str(date_v))}</v8:date>') - lines.append(f'{indent}\t</dcsset:right>') - elif str(val) == '_': - # "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right> - # (напр. <dcsset:right xsi:type="dcscor:Field"/> — сравнение с незаданным полем). - vt = str(item['valueType']) if item.get('valueType') else 'xs:string' - lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}"/>') - elif val is not None: - vt = _value_type_for(val, item.get('valueType')) - v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val)) - lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>') - if item.get('presentation'): - emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) - if item.get('viewMode'): - lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') - if item.get('userSettingID'): - uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) - lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') - if item.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) - lines.append(f'{indent}</dcsset:item>') - - -def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None): - has_items = bool(items) and len(items) > 0 - has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) - if not has_items and not has_block_meta: - return - lines.append(f'{indent}<dcsset:filter>') - for item in (items or []): - if isinstance(item, str): - parsed = parse_filter_shorthand(item) - obj = {'field': parsed['field'], 'op': parsed['op']} - if parsed['use'] is False: - obj['use'] = False - if parsed['value'] is not None: - obj['value'] = parsed['value'] - if parsed.get('valueType'): - obj['valueType'] = parsed['valueType'] - if parsed.get('userSettingID'): - obj['userSettingID'] = parsed['userSettingID'] - if parsed.get('viewMode'): - obj['viewMode'] = parsed['viewMode'] - emit_filter_item(lines, obj, f'{indent}\t') - else: - emit_filter_item(lines, item, f'{indent}\t') - if block_view_mode is not None: - lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') - if block_user_setting_id is not None: - uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) - lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') - lines.append(f'{indent}</dcsset:filter>') - - -def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None): - has_items = bool(items) and len(items) > 0 - has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) - if not has_items and not has_block_meta: - return - lines.append(f'{indent}<dcsset:order>') - for item in (items or []): - if isinstance(item, str): - if item == 'Auto': - if not skip_auto: - lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>') - else: - parts = re.split(r'\s+', item) - field = parts[0] - direction = 'Asc' - if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]): - direction = 'Desc' - elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]): - direction = 'Asc' - lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">') - lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>') - lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>') - lines.append(f'{indent}\t</dcsset:item>') - else: - if item.get('field') == 'Auto' or item.get('type') == 'auto': - if not skip_auto: - lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>') - continue - direction = str(item['direction']) if item.get('direction') else 'Asc' - if re.match(r'(?i)^(desc|убыв)', direction): - direction = 'Desc' - elif re.match(r'(?i)^(asc|возр)', direction): - direction = 'Asc' - lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">') - if item.get('use') is False: - lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>') - lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(item.get("field", "")))}</dcsset:field>') - lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>') - if item.get('viewMode'): - lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') - lines.append(f'{indent}\t</dcsset:item>') - if block_view_mode is not None: - lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') - if block_user_setting_id is not None: - uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) - lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') - lines.append(f'{indent}</dcsset:order>') - - -def emit_appearance_value(lines, key, val, indent): - lines.append(f'{indent}<dcscor:item xsi:type="dcsset:SettingsParameterValue">') - - def _has_key(o, k): - return isinstance(o, dict) and (k in o) - - def _get(o, k): - return o.get(k) if isinstance(o, dict) else None - - is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line') - use_wrapper = False - inner_val = val - nested_items = None - if is_top_level_line: - if _has_key(val, 'use') and (_get(val, 'use') is False): - use_wrapper = True - if _has_key(val, 'items'): - nested_items = _get(val, 'items') - elif _has_key(val, 'value') and isinstance(val, dict): - inner_val = _get(val, 'value') - if _has_key(val, 'use') and (_get(val, 'use') is False): - use_wrapper = True - if _has_key(val, 'items'): - nested_items = _get(val, 'items') - if use_wrapper: - lines.append(f'{indent}\t<dcscor:use>false</dcscor:use>') - lines.append(f'{indent}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>') - - is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font' - is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line') - is_dict = isinstance(inner_val, dict) - if is_line_dict: - lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0 - lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false' - ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None' - lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Line" width="{lw}" gap="{lg}">') - lines.append(f'{indent}\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">{esc_xml(ls)}</v8ui:style>') - lines.append(f'{indent}\t</dcscor:value>') - elif is_font_dict: - attr_parts = [] - for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): - if attr_name in inner_val: - av = inner_val[attr_name] - if av is not None: - attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"') - lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Font" {" ".join(attr_parts)}/>') - elif is_dict and _has_key(inner_val, 'field'): - # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки - lines.append(f'{indent}\t<dcscor:value xsi:type="dcscor:Field">{esc_xml(str(_get(inner_val, "field")))}</dcscor:value>') - elif is_dict: - # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value - emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val, xsi_type='v8:LocalStringType') - else: - actual_val = str(inner_val) - key_type_map = { - 'Размещение': 'dcscor:DataCompositionTextPlacementType', - 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign', - 'ВертикальноеПоложение': 'v8ui:VerticalAlign', - 'ОриентацияТекста': 'xs:decimal', - 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement', - 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType', - } - key_type = key_type_map.get(key) - if key_type: - lines.append(f'{indent}\t<dcscor:value xsi:type="{key_type}">{esc_xml(actual_val)}</dcscor:value>') - elif re.match(r'^(style|web|win):', actual_val): - lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>') - elif actual_val == 'true' or actual_val == 'false': - lines.append(f'{indent}\t<dcscor:value xsi:type="xs:boolean">{actual_val}</dcscor:value>') - elif key == 'Текст' or key == 'Заголовок' or key == 'Формат': - # Голая строка = плоский xs:string (нелокализованный литерал). Локализуемый → объект {ru,en}. - # Пустая строка → самозакрывающийся тег (как у платформы). - if actual_val == '': - lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string"/>') - else: - lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>') - elif re.match(r'^-?\d+(\.\d+)?$', actual_val): - lines.append(f'{indent}\t<dcscor:value xsi:type="xs:decimal">{actual_val}</dcscor:value>') - elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы': - lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>') - else: - lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>') - if nested_items: - if isinstance(nested_items, dict): - for nk, nv in nested_items.items(): - emit_appearance_value(lines, nk, nv, f'{indent}\t') - lines.append(f'{indent}</dcscor:item>') - - -def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance'): - has_items = bool(items) and len(items) > 0 - has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) - if not has_items and not has_block_meta: - return - lines.append(f'{indent}<{wrap_tag}>') - for ca in (items or []): - lines.append(f'{indent}\t<dcsset:item>') - if ca.get('use') is False: - lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>') - if ca.get('selection') and len(ca['selection']) > 0: - lines.append(f'{indent}\t\t<dcsset:selection>') - for sel in ca['selection']: - lines.append(f'{indent}\t\t\t<dcsset:item>') - lines.append(f'{indent}\t\t\t\t<dcsset:field>{esc_xml(str(sel))}</dcsset:field>') - lines.append(f'{indent}\t\t\t</dcsset:item>') - lines.append(f'{indent}\t\t</dcsset:selection>') - else: - lines.append(f'{indent}\t\t<dcsset:selection/>') - if ca.get('filter') and len(ca['filter']) > 0: - emit_filter(lines, ca['filter'], f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t<dcsset:filter/>') - if ca.get('appearance'): - lines.append(f'{indent}\t\t<dcsset:appearance>') - for k, v in ca['appearance'].items(): - emit_appearance_value(lines, k, v, f'{indent}\t\t\t') - lines.append(f'{indent}\t\t</dcsset:appearance>') - if ca.get('presentation'): - if isinstance(ca['presentation'], dict): - # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) - lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="v8:LocalStringType">') - emit_ml_items(lines, f'{indent}\t\t\t', ca['presentation']) - lines.append(f'{indent}\t\t</dcsset:presentation>') - else: - lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="xs:string">{esc_xml(str(ca["presentation"]))}</dcsset:presentation>') - if ca.get('viewMode'): - lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(ca["viewMode"]))}</dcsset:viewMode>') - if ca.get('userSettingID'): - uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID']) - lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') - if ca.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation']) - if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0: - use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header', - 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader', - 'overallResourceFieldsHeader'] - sset = {str(n): True for n in ca['useInDontUse']} - for n in use_in_order: - if n in sset: - tag = 'useIn' + n[0].upper() + n[1:] - lines.append(f'{indent}\t\t<dcsset:{tag}>DontUse</dcsset:{tag}>') - lines.append(f'{indent}\t</dcsset:item>') - if block_view_mode is not None: - lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') - if block_user_setting_id is not None: - uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) - lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') - lines.append(f'{indent}</{wrap_tag}>') - - -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 - - -# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё -# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. -_seen_element_names = set() # пул имён элементов (глобально по всей форме) - -def _ensure_unique(name, seen, kind): - if name in seen: - print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr) - sys.exit(1) - seen.add(name) - - -# --- 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"], - "radio": ["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", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", - "button", "picture", "picField", "calendar", "cmdBar", "popup", - "showInHeader", - "radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode", - "name", "path", "title", "tooltip", "tooltipRepresentation", "extendedTooltip", - "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", - "events", "on", "handlers", - "selectionMode", "showCurrentDate", "widthInMonths", "heightInMonths", "showMonthsPanel", - "titleLocation", "representation", "width", "height", - "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", - "maxWidth", "maxHeight", - "groupHorizontalAlign", "groupVerticalAlign", "horizontalAlign", - "multiLine", "passwordMode", "choiceButton", "clearButton", - "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", - "textEdit", "choiceList", - "wrap", "openButton", "listChoiceMode", "showInHeader", "showInFooter", - "extendedEditMultipleValues", "chooseType", "autoCellHeight", - "choiceButtonRepresentation", "footerHorizontalAlign", "headerHorizontalAlign", - "format", "editFormat", "choiceParameters", "choiceParameterLinks", "typeLink", - "hyperlink", "formatted", - "collapsedTitle", "showTitle", "united", "collapsed", "behavior", - "children", "columns", - "changeRowSet", "changeRowOrder", "autoInsertNewRow", "rowFilter", "header", "footer", - "commandBarLocation", "searchStringLocation", "viewStatusLocation", "searchControlLocation", - "excludedCommands", - "pagesRepresentation", - "type", "command", "commandName", "stdCommand", "defaultButton", "locationInCommandBar", "displayImportance", - "commandBar", "contextMenu", "commandSource", - "src", "valuesPicture", "loadTransparent", "headerPicture", "footerPicture", - "autofill", - "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", - "rowSelectionMode", "verticalLines", "horizontalLines", - "rowPictureDataPath", "tableAutofill", "heightInTableRows", - "multipleChoice", "searchOnInput", "shortcut", - # dynamic-list table block - "defaultItem", "useAlternationRowColor", "fileDragMode", "autoRefresh", - "autoRefreshPeriod", "choiceFoldersAndItems", "restoreCurrentRow", "showRoot", - "allowRootChoice", "updateOnDataChange", "allowGettingCurrentRowURL", - "userSettingsGroup", "rowsPicture", - # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице - "autoCmdBar", - # дополнения командной панели таблицы (тип-ключи + свойства) - "searchString", "viewStatus", "searchControl", "source", "horizontalLocation", "additions", - # generic-скаляры (pass-through) - "verticalAlign", "throughAlign", "enableContentChange", "pictureSize", "titleHeight", - "childItemsWidth", "showLeftMargin", "cellHyperlink", "viewMode", "verticalScrollBar", - "rowInputMode", "mask", "createButton", "fixingInTable", "verticalSpacing", - # InputField choice-скаляры - "choiceListButton", "quickChoice", "autoChoiceIncomplete", - "choiceForm", "choiceHistoryOnInput", "footerDataPath", "minValue", "maxValue", - # Button — пометка toggle-кнопки - "checked", - # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры - "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", - "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram", "ganttTable", - "showPercent", "largeStep", "markingStep", "step", - "horizontalScrollBar", "viewScalingMode", "output", "selectionShowMode", "protection", - "edit", "showGrid", "showGroups", "showHeaders", "showRowAndColumnNames", "showCellNames", - "pointerType", "drawingSelectionShowMode", "warningOnEditRepresentation", "markingAppearance", - # report-form контекст (generic-скаляры элементов) - "horizontalSpacing", "representationInContextMenu", "settingsNamedItemDetailedRepresentation", - # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель - "itemHeight", "dropListWidth", "choiceButtonPicture", "transparentPixel", - # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы - "titleDataPath", "extendedEdit", "maxRowsCount", "autoMaxRowsCount", "heightControlVariant", - "warningOnEdit", "nonselectedPictureText", "editTextUpdate", "footerText", -} - -# picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка -# у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. -# pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей -# (<Group>Horizontal</Group>), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. -TYPE_KEYS = ["columnGroup", "buttonGroup", "pages", "page", "group", "input", "check", "radio", "label", "labelField", "table", - "button", "calendar", "cmdBar", "popup", "searchString", "viewStatus", "searchControl", "picField", "picture", - "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", - "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram"] - -# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) -ELEMENT_TYPE_SYNONYMS = { - "commandBar": "cmdBar", - "autoCommandBar": "autoCmdBar", - "КоманднаяПанель": "cmdBar", - "InputField": "input", - "ПолеВвода": "input", - "CheckBoxField": "check", - "ПолеФлажка": "check", - "RadioButtonField": "radio", - "ПолеПереключателя": "radio", - "radioButton": "radio", - "PictureField": "picField", - "ПолеКартинки": "picField", - "LabelField": "labelField", - "ПолеНадписи": "labelField", - "CalendarField": "calendar", - "ПолеКалендаря": "calendar", - "LabelDecoration": "label", - "Надпись": "label", - "PictureDecoration": "picture", - "Картинка": "picture", - "UsualGroup": "group", - "Группа": "group", - "ОбычнаяГруппа": "group", - "ColumnGroup": "columnGroup", - "ГруппаКолонок": "columnGroup", - "Pages": "pages", - "ГруппаСтраниц": "pages", - "Page": "page", - "Страница": "page", - "Table": "table", - "Таблица": "table", - "Button": "button", - "Кнопка": "button", - "Popup": "popup", - "ВсплывающееМеню": "popup", - # дополнения командной панели таблицы — forgiving: XML-тег/Type/рус.имя → канон - "SearchStringAddition": "searchString", - "SearchStringRepresentation": "searchString", - "строкаПоиска": "searchString", - "отображениеСтрокиПоиска": "searchString", - "Отображение строки поиска": "searchString", - "ViewStatusAddition": "viewStatus", - "ViewStatusRepresentation": "viewStatus", - "состояниеПросмотра": "viewStatus", - "Состояние просмотра": "viewStatus", - "SearchControlAddition": "searchControl", - "SearchControl": "searchControl", - "управлениеПоиском": "searchControl", - "Управление поиском": "searchControl", - # Спец-поля (документ/датчик) — XML-имя/рус. → канон - "SpreadSheetDocumentField": "spreadsheet", - "ПолеТабличногоДокумента": "spreadsheet", - "HTMLDocumentField": "html", - "ПолеHTMLДокумента": "html", - "TextDocumentField": "textDoc", - "ПолеТекстовогоДокумента": "textDoc", - "FormattedDocumentField": "formattedDoc", - "ПолеФорматированногоДокумента": "formattedDoc", - "ProgressBarField": "progressBar", - "ПолеИндикатора": "progressBar", - "TrackBarField": "trackBar", - "ПолеПолосыРегулирования": "trackBar", - "ChartField": "chart", - "ПолеДиаграммы": "chart", - "GanttChartField": "ganttChart", - "ПолеДиаграммыГанта": "ganttChart", - "GraphicalSchemaField": "graphicalSchema", - "ПолеГрафическойСхемы": "graphicalSchema", - "PlannerField": "planner", - "ПолеПланировщика": "planner", - "PeriodField": "periodField", - "ПолеПериода": "periodField", - "DendrogramField": "dendrogram", - "ПолеДендрограммы": "dendrogram", -} - -# Тип-синонимы, применяемые ТОЛЬКО к строковому значению (имя элемента); объект/массив -# у того же слова — companion-панель (свойство), см. normalize_panel_synonyms. -STR_ONLY_TYPE_SYNONYMS = {"commandBar", "autoCommandBar", "КоманднаяПанель"} - -# Companion-панели как СВОЙСТВА (значение объект/массив): синоним → каноника. -PANEL_SYNONYMS = { - 'commandBar': ['commandBar', 'autoCommandBar', 'AutoCommandBar', 'autoCmdBar', 'cmdBar', 'КоманднаяПанель'], - 'contextMenu': ['contextMenu', 'ContextMenu', 'КонтекстноеМеню'], -} - - -def normalize_panel_synonyms(el): - if not isinstance(el, dict): - return - for canon, syns in PANEL_SYNONYMS.items(): - for syn in syns: - if syn in el and isinstance(el[syn], (list, dict)): - if syn != canon and canon not in el: - el[canon] = el.pop(syn) - break - - -# Maps Russian/English root of typed reference path to canonical English root -REF_ROOT_SYNONYMS = { - "Перечисление": "Enum", - "Справочник": "Catalog", - "Документ": "Document", - "ПланСчетов": "ChartOfAccounts", - "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", - "ПланВидовРасчета": "ChartOfCalculationTypes", - "ПланВидовРасчёта": "ChartOfCalculationTypes", - "ПланОбмена": "ExchangePlan", - "БизнесПроцесс": "BusinessProcess", - "Задача": "Task", - "РегистрСведений": "InformationRegister", - "РегистрНакопления": "AccumulationRegister", - "РегистрБухгалтерии": "AccountingRegister", - "РегистрРасчета": "CalculationRegister", - "РегистрРасчёта": "CalculationRegister", - "ЖурналДокументов": "DocumentJournal", - "КритерийОтбора": "FilterCriterion", -} -ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} - - -def normalize_meta_type_ref(ref): - # "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений - if not ref: - return ref - dot = ref.find('.') - if dot < 1: - return ref - root = ref[:dot] - if root in REF_ROOT_SYNONYMS: - return REF_ROOT_SYNONYMS[root] + ref[dot:] - return ref - - -def normalize_choice_value(value): - """Returns dict {xsi_type, text} for a choiceList item value.""" - if isinstance(value, bool): - return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} - if isinstance(value, (int, float)): - return {"xsi_type": "xs:decimal", "text": str(value)} - - s = "" if value is None else str(value) - if not s: - return {"xsi_type": "xs:string", "text": ""} - - # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime - if re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}', s): - return {"xsi_type": "xs:dateTime", "text": s} - - parts = s.split(".") - if len(parts) >= 2: - root = parts[0] - canon_root = None - if root in REF_ROOT_SYNONYMS: - canon_root = REF_ROOT_SYNONYMS[root] - elif root in REF_ROOT_SYNONYMS.values(): - canon_root = root - - if canon_root: - type_name = parts[1] - normalized = None - if canon_root == "Enum": - if len(parts) == 3 and parts[2] == 'EmptyRef': - # "Enum.X.EmptyRef" — пустая ссылка, НЕ значение перечисления (без .EnumValue.) - normalized = f"Enum.{type_name}.EmptyRef" - elif len(parts) == 3: - normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" - elif len(parts) >= 4: - member = parts[2] - if member in ENUM_VALUE_SYNONYMS: - rest = ".".join(parts[3:]) - else: - rest = ".".join(parts[2:]) - normalized = f"Enum.{type_name}.EnumValue.{rest}" - else: - if len(parts) >= 3: - tail = ".".join(parts[1:]) - normalized = f"{canon_root}.{tail}" - - if normalized: - return {"xsi_type": "xr:DesignTimeRef", "text": normalized} - - return {"xsi_type": "xs:string", "text": s} - - -def emit_choice_presentation(lines, pres, indent): - """Accepts None/empty → <Presentation/>; str → ru only; dict → multi-lang.""" - if pres is None or (isinstance(pres, str) and pres == ""): - lines.append(f"{indent}<Presentation/>") - return - - if isinstance(pres, str): - pairs = [("ru", pres)] - elif isinstance(pres, dict): - pairs = [(str(k), str(v)) for k, v in pres.items()] - else: - pairs = [("ru", str(pres))] - - lines.append(f"{indent}<Presentation>") - for lang, content in pairs: - lines.append(f"{indent}\t<v8:item>") - lines.append(f"{indent}\t\t<v8:lang>{lang}</v8:lang>") - lines.append(f"{indent}\t\t<v8:content>{esc_xml(content)}</v8:content>") - lines.append(f"{indent}\t</v8:item>") - lines.append(f"{indent}</Presentation>") - - -def choice_value_tag(norm): - # <Value> для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). - if not norm["text"]: - return f'<Value xsi:type="{norm["xsi_type"]}"/>' - return f'<Value xsi:type="{norm["xsi_type"]}">{esc_xml(norm["text"])}</Value>' - - -def emit_choice_list(lines, el, indent): - # <ChoiceList> — у RadioButtonField и InputField. Элемент: { value, presentation?/title? }. - choice_list = el.get('choiceList') or [] - if not choice_list: - return - lines.append(f'{indent}<ChoiceList>') - item_indent = f'{indent}\t' - for item in choice_list: - if not isinstance(item, dict): - continue - val_raw = item.get('value', item.get('значение')) - has_pres = any(k in item for k in ('presentation', 'представление', 'title')) - pres_raw = item.get('presentation', item.get('представление', item.get('title'))) - - # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — - # переопределяет авто-детект (normalize_choice_value вывела бы xs:string). - vt_raw = item.get('valueType') - if vt_raw: - norm = {'xsi_type': str(vt_raw), 'text': '' if val_raw is None else str(val_raw)} - else: - norm = normalize_choice_value(val_raw) - - if not has_pres: - if norm['xsi_type'] == 'xr:DesignTimeRef': - tail = norm['text'].split('.')[-1] - pres_raw = title_from_name(tail) - else: - pres_raw = norm['text'] - - lines.append(f'{item_indent}<xr:Item>') - val_indent = f'{item_indent}\t' - lines.append(f'{val_indent}<xr:Presentation/>') - lines.append(f'{val_indent}<xr:CheckState>0</xr:CheckState>') - lines.append(f'{val_indent}<xr:Value xsi:type="FormChoiceListDesTimeValue">') - emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') - lines.append(f'{val_indent}\t{choice_value_tag(norm)}') - lines.append(f'{val_indent}</xr:Value>') - lines.append(f'{item_indent}</xr:Item>') - lines.append(f'{indent}</ChoiceList>') - - -def get_el_prop(obj, names): - # Читает свойство из dict по списку синонимов (первый найденный, иначе None). - if not isinstance(obj, dict): - return None - for n in names: - if n in obj: - return obj[n] - return None - - -def to_scalar_literal(s): - # Литерал shorthand → тип: true/false → bool, целое/дробное → число, иначе строка. - t = str(s).strip() - if t.lower() == 'true': - return True - if t.lower() == 'false': - return False - if re.fullmatch(r'-?\d+', t): - return int(t) - if re.fullmatch(r'-?\d+\.\d+', t): - return float(t) - return t - - -def from_choice_param_shorthand(s): - # "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. - eq = s.find('=') - if eq < 0: - return {'name': s.strip()} - name = s[:eq].strip() - rest = s[eq + 1:] - if ',' in rest: - return {'name': name, 'value': [to_scalar_literal(p) for p in rest.split(',')]} - return {'name': name, 'value': to_scalar_literal(rest)} - - -def from_choice_param_link_shorthand(s): - # "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. - eq = s.find('=') - if eq < 0: - return {'name': s.strip()} - o = {'name': s[:eq].strip()} - rest = s[eq + 1:].strip() - m = re.fullmatch(r'(.*):(Clear|DontChange|очистить|неизменять)', rest, re.IGNORECASE) - if m: - o['dataPath'] = m.group(1).strip() - o['valueChange'] = m.group(2) - else: - o['dataPath'] = rest - return o - - -def from_type_link_shorthand(s): - # "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. - m = re.fullmatch(r'(.*)#(\d+)', str(s)) - if m: - return {'dataPath': m.group(1).strip(), 'linkItem': int(m.group(2))} - return {'dataPath': str(s).strip()} - - -def emit_choice_param_value(lines, value, indent): - # Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): <Presentation/> + <Value>. - # Скаляр → один Value; массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. - lines.append(f'{indent}<Presentation/>') - if isinstance(value, (list, tuple)): - lines.append(f'{indent}<Value xsi:type="v8:FixedArray">') - for v in value: - norm = normalize_choice_value(v) - lines.append(f'{indent}\t<v8:Value xsi:type="FormChoiceListDesTimeValue">') - lines.append(f'{indent}\t\t<Presentation/>') - lines.append(f'{indent}\t\t{choice_value_tag(norm)}') - lines.append(f'{indent}\t</v8:Value>') - lines.append(f'{indent}</Value>') - else: - norm = normalize_choice_value(value) - lines.append(f'{indent}{choice_value_tag(norm)}') - - -def emit_choice_parameters(lines, el, indent): - # <ChoiceParameters> (параметры выбора поля ввода) — [{name, value}]. value через - # normalize_choice_value; массив значений → FixedArray. Рус. синонимы имя/значение. - cp = el.get('choiceParameters') or [] - if not cp: - return - lines.append(f'{indent}<ChoiceParameters>') - for item in cp: - if isinstance(item, str): - item = from_choice_param_shorthand(item) - name = get_el_prop(item, ('name', 'имя')) - has_val = isinstance(item, dict) and ('value' in item or 'значение' in item) - val = get_el_prop(item, ('value', 'значение')) - name_s = '' if name is None else str(name) - lines.append(f'{indent}\t<app:item name="{esc_xml(name_s)}">') - # Параметр выбора без значения → <app:value xsi:nil="true"/> (платформа, 13 в корпусе); - # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. - if not has_val: - lines.append(f'{indent}\t\t<app:value xsi:nil="true"/>') - else: - lines.append(f'{indent}\t\t<app:value xsi:type="FormChoiceListDesTimeValue">') - emit_choice_param_value(lines, val, f'{indent}\t\t\t') - lines.append(f'{indent}\t\t</app:value>') - lines.append(f'{indent}\t</app:item>') - lines.append(f'{indent}</ChoiceParameters>') - - -def emit_choice_parameter_links(lines, el, indent): - # <ChoiceParameterLinks> (связи параметров выбора) — [{name, dataPath, valueChange?}]. - # valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. - cpl = el.get('choiceParameterLinks') or [] - if not cpl: - return - lines.append(f'{indent}<ChoiceParameterLinks>') - for lk in cpl: - if isinstance(lk, str): - lk = from_choice_param_link_shorthand(lk) - name = get_el_prop(lk, ('name', 'имя')) - dp = get_el_prop(lk, ('dataPath', 'path', 'путь')) - vc_raw = get_el_prop(lk, ('valueChange', 'режимИзменения')) - vc = 'Clear' - if vc_raw: - s = str(vc_raw).lower() - if s in ('clear', 'очистить', 'очистка'): - vc = 'Clear' - elif s in ('dontchange', 'неизменять', 'неменять', 'нет'): - vc = 'DontChange' - else: - vc = str(vc_raw) - name_s = '' if name is None else str(name) - dp_s = '' if dp is None else str(dp) - lines.append(f'{indent}\t<xr:Link>') - lines.append(f'{indent}\t\t<xr:Name>{esc_xml(name_s)}</xr:Name>') - lines.append(f'{indent}\t\t<xr:DataPath xsi:type="xs:string">{esc_xml(dp_s)}</xr:DataPath>') - lines.append(f'{indent}\t\t<xr:ValueChange>{vc}</xr:ValueChange>') - lines.append(f'{indent}\t</xr:Link>') - lines.append(f'{indent}</ChoiceParameterLinks>') - - -def emit_type_link(lines, el, indent): - # <TypeLink> (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. - tl = el.get('typeLink') - if not tl: - return - if isinstance(tl, str): - tl = from_type_link_shorthand(tl) - dp = get_el_prop(tl, ('dataPath', 'path', 'путь')) - li = get_el_prop(tl, ('linkItem', 'элементСвязи')) - if li is None: - li = 0 - dp_s = '' if dp is None else str(dp) - lines.append(f'{indent}<TypeLink>') - lines.append(f'{indent}\t<xr:DataPath>{esc_xml(dp_s)}</xr:DataPath>') - lines.append(f'{indent}\t<xr:LinkItem>{li}</xr:LinkItem>') - lines.append(f'{indent}</TypeLink>') - - -def normalize_radio_button_type(raw): - if not raw: - return "Auto" - s = str(raw).strip().lower() - if s in ("auto", "авто"): - return "Auto" - if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): - return "RadioButtons" - if s in ("tumbler", "тумблер"): - return "Tumbler" - return str(raw).strip() - - -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, '')) - - -# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. -# Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции). -# Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён). -def get_event_pairs(el, element_name): - pairs = [] - events = el.get('events') - if events: - for ev_name, val in events.items(): - handler = '' if val is None else str(val) - if not handler: - handler = get_handler_name(element_name, ev_name) - pairs.append((ev_name, handler)) - elif el.get('on'): - handlers = el.get('handlers') or {} - for evt in el['on']: - evt_name = str(evt) - if handlers.get(evt_name): - handler = str(handlers[evt_name]) - else: - handler = get_handler_name(element_name, evt_name) - pairs.append((evt_name, handler)) - return pairs - - -# Проверить, подключено ли событие к элементу (в любом из форматов). -def test_element_event(el, event_name): - events = el.get('events') - if events and event_name in events: - return True - return event_name in (el.get('on') or []) - - -def emit_events(lines, el, element_name, indent, type_key): - pairs = get_event_pairs(el, element_name) - if not pairs: - return - - # Validate event names - if type_key and type_key in KNOWN_EVENTS: - allowed = KNOWN_EVENTS[type_key] - for ev_name, _ in pairs: - if allowed and str(ev_name) not in allowed: - print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") - - lines.append(f"{indent}<Events>") - for ev_name, handler in pairs: - lines.append(f'{indent}\t<Event name="{ev_name}">{handler}</Event>') - lines.append(f"{indent}</Events>") - - -# Детектор «настоящей» inline-разметки (1С: <link>/<b>/<color>/… и </>). Должен быть -# идентичен form-decompile/form-compile.ps1, иначе гибрид-раундтрип поедет. -_FMT_MARKUP_RE = re.compile(r'</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)', re.I) - - -def _has_real_markup(text): - if text is None: - return False - vals = list(text.values()) if isinstance(text, dict) else [text] - return any(_FMT_MARKUP_RE.search(str(v)) for v in vals) - - -def resolve_ml_formatted(val): - # {text, formatted} = явный override; строка/мапа → авто-детект formatted - if isinstance(val, dict) and 'text' in val: - return val['text'], bool(val.get('formatted')) - return val, _has_real_markup(val) - - -# ExtendedTooltip — это LabelDecoration: own-content (layout/оформление/флаги/hyperlink) ±текст. -# Признак структурированной формы: объект с любым НЕ-текстовым ключом ({text,formatted}/{ru,en} → текст). -COMPANION_STRUCT_KEYS = { - 'width', 'autoMaxWidth', 'maxWidth', 'height', 'autoMaxHeight', 'maxHeight', 'verticalAlign', 'titleHeight', - 'horizontalStretch', 'verticalStretch', 'horizontalAlign', 'groupHorizontalAlign', 'groupVerticalAlign', - 'visible', 'hidden', 'enabled', 'disabled', 'hyperlink', 'events', - 'textColor', 'backColor', 'borderColor', 'font', 'border', 'цветтекста', 'цветфона', 'цветрамки', 'шрифт', 'рамка', -} - - -def emit_companion_title(lines, content, indent): - text, fmt = resolve_ml_formatted(content) - lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') - emit_ml_items(lines, f'{indent}\t', text) - lines.append(f'{indent}') - - -def emit_companion(lines, tag, name, indent, content=None): - cid = new_id() - has_content = content is not None and not (isinstance(content, str) and content == '') - if not has_content: - lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') - return - inner = f'{indent}\t' - lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') - if isinstance(content, dict) and any(k in content for k in COMPANION_STRUCT_KEYS): - # own-content ПЕРЕД Title (в корпусе layout-first 582 vs 10). - emit_common_flags(lines, content, inner) - if content.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, content, inner) - emit_appearance(lines, content, inner, 'decoration') - if 'text' in content: - emit_companion_title(lines, content, inner) - # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) - emit_events(lines, content, name, inner, 'label') - else: - emit_companion_title(lines, content, inner) - lines.append(f'{indent}') - - -def emit_companion_panel(lines, tag, name, indent, panel): - # Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, horizontalAlign?, children?[] } - # или массив = shorthand для { children }. Пусто/нет → self-closing. - cid = new_id() - autofill = None - halign = None - children = None - if isinstance(panel, list): - children = panel - elif panel is not None: - if panel.get('autofill') is not None: - autofill = bool(panel.get('autofill')) - if panel.get('horizontalAlign'): - halign = str(panel.get('horizontalAlign')) - children = panel.get('children') - has_children = bool(children) and len(children) > 0 - # Платформа пишет только при false; true = дефолт (тег опускается). - emit_af_false = (autofill is False) - if not emit_af_false and not has_children and not halign: - lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') - return - lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') - if halign: - lines.append(f'{indent}\t{halign}') - if emit_af_false: - lines.append(f'{indent}\tfalse') - if has_children: - lines.append(f'{indent}\t') - for c in children: - emit_element(lines, c, f'{indent}\t\t', in_cmd_bar=True) - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type + суффикс имени. -ADDITION_TYPE_MAP = { - 'searchString': {'tag': 'SearchStringAddition', 'type': 'SearchStringRepresentation', 'suffix': 'СтрокаПоиска'}, - 'viewStatus': {'tag': 'ViewStatusAddition', 'type': 'ViewStatusRepresentation', 'suffix': 'СостояниеПросмотра'}, - 'searchControl': {'tag': 'SearchControlAddition', 'type': 'SearchControl', 'suffix': 'УправлениеПоиском'}, -} -ADDITION_KEY_SYNONYMS = { - 'searchString': ['SearchStringAddition', 'SearchStringRepresentation', 'строкаПоиска', 'отображениеСтрокиПоиска'], - 'viewStatus': ['ViewStatusAddition', 'ViewStatusRepresentation', 'состояниеПросмотра'], - 'searchControl': ['SearchControlAddition', 'SearchControl', 'управлениеПоиском'], -} -# Имя текущей таблицы — дефолт source для кастомных дополнений в commandBar. -_current_table_name = {'name': None} - - -def get_hlocation(el): - # HorizontalLocation: auto (дефолт, опускаем) / left / right; forgiving + рус. - if not isinstance(el, dict): - return None - v = el.get('horizontalLocation') - if not v: - return None - s = str(v).lower() - if s in ('auto', 'авто'): - return None - if s in ('left', 'слева', 'лево'): - return 'Left' - if s in ('right', 'справа', 'право'): - return 'Right' - if s in ('center', 'центр', 'по центру'): - return 'Center' - return str(v) - - -def emit_addition_body(lines, props, source, src_type, add_name, indent): - # Тело дополнения: AdditionSource + свойства (как у поля) + companions. props может быть None. - inner = f'{indent}\t' - lines.append(f'{inner}') - lines.append(f'{inner}\t{source}') - lines.append(f'{inner}\t{src_type}') - lines.append(f'{inner}') - if props: - if props.get('title'): - emit_mltext(lines, inner, 'Title', props['title']) - emit_common_flags(lines, props, inner) - if props.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', props['tooltip']) - if props.get('tooltipRepresentation'): - lines.append(f'{inner}{props["tooltipRepresentation"]}') - hl = get_hlocation(props) - if hl: - lines.append(f'{inner}{hl}') - emit_layout(lines, props, inner) - emit_appearance(lines, props, inner, 'field') - emit_companion(lines, 'ContextMenu', f'{add_name}КонтекстноеМеню', inner) - emit_companion(lines, 'ExtendedTooltip', f'{add_name}РасширеннаяПодсказка', inner) - - -def emit_addition(lines, el, name, eid, type_key, indent): - # Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. - m = ADDITION_TYPE_MAP[type_key] - source = el.get('source') or _current_table_name['name'] or '' - lines.append(f'{indent}<{m["tag"]} name="{name}" id="{eid}"{di_attr(el)}>') - emit_addition_body(lines, el, source, m['type'], name, indent) - lines.append(f'{indent}') - - -def emit_table_addition(lines, type_key, table_name, indent, override=None): - # Стандартное табличное дополнение (авто-генерация). override — объект отклонений из карты additions. - m = ADDITION_TYPE_MAP[type_key] - add_name = f'{table_name}{m["suffix"]}' - aid = new_id() - lines.append(f'{indent}<{m["tag"]} name="{add_name}" id="{aid}">') - emit_addition_body(lines, override, table_name, m['type'], add_name, indent) - lines.append(f'{indent}') - - -def get_addition_override(additions, type_key): - # Прочитать override-объект для типа из per-table карты additions (с синонимами). - if not isinstance(additions, dict): - return None - for k in [type_key] + ADDITION_KEY_SYNONYMS[type_key]: - if k in additions: - return additions[k] - return None - - -# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). -# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). -# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. -# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. -def emit_xr_flag(lines, tag, val, indent): - if val is None: - return - if isinstance(val, bool): - lines.append(f"{indent}<{tag}>") - lines.append(f"{indent}\t{'true' if val else 'false'}") - lines.append(f"{indent}") - return - # объектная форма { common, roles } - common = bool(val.get('common')) if val.get('common') is not None else False - lines.append(f"{indent}<{tag}>") - lines.append(f"{indent}\t{'true' if common else 'false'}") - roles = val.get('roles') - if roles: - for rname, rval in roles.items(): - # Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". - # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. - rn = re.sub(r'^(Role|Роль)\.', '', rname) - if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', rn): - rn = "Role." + rn - lines.append(f"{indent}\t{'true' if rval else 'false'}") - lines.append(f"{indent}") - - -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('userVisible') is not None: - emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent) - 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") - - -# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. -def emit_common_element_props(lines, el, indent): - if el.get('defaultItem') is True: - lines.append(f"{indent}true") - if 'skipOnInput' in el and el['skipOnInput'] is not None: - siv = 'true' if el['skipOnInput'] is True else 'false' - lines.append(f"{indent}{siv}") - # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) - if el.get('enableStartDrag') is not None: - lines.append(f'{indent}{"true" if el["enableStartDrag"] else "false"}') - if el.get('fileDragMode'): - lines.append(f"{indent}{el['fileDragMode']}") - # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» - for key, tag in (('showInHeader', 'ShowInHeader'), ('showInFooter', 'ShowInFooter'), ('autoCellHeight', 'AutoCellHeight')): - if el.get(key) is not None: - lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') - if el.get('footerHorizontalAlign'): - lines.append(f"{indent}{el['footerHorizontalAlign']}") - if el.get('headerHorizontalAlign'): - lines.append(f"{indent}{el['headerHorizontalAlign']}") - - -def emit_picture_ref(lines, val, pic_tag, indent): - """Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). - Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). - Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. - src с префиксом "abs:" → встроенная картинка ; иначе .""" - if not val: - return - tpx = None - if isinstance(val, str): - src, lt = val, False - else: - src = val.get('src') - lt = val.get('loadTransparent') is True - tpx = val.get('transparentPixel') - if not src: - return - src_str = str(src) - lines.append(f"{indent}<{pic_tag}>") - if src_str.startswith('abs:'): - lines.append(f"{indent}\t{esc_xml(src_str[4:])}") - else: - lines.append(f"{indent}\t{esc_xml(src_str)}") - lines.append(f'{indent}\t{"true" if lt else "false"}') - if tpx: - lines.append(f'{indent}\t') - lines.append(f"{indent}") - - -def emit_column_pics(lines, el, indent): - """Картинки заголовка/подвала колонки поля — по схеме сразу после , - перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь).""" - emit_picture_ref(lines, el.get('headerPicture'), 'HeaderPicture', indent) - emit_picture_ref(lines, el.get('footerPicture'), 'FooterPicture', indent) - - -def emit_command_picture(lines, pic, elem_lt, indent): - """ кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false - (обратная конвенция относительно header/values-картинок). Прощающий ввод: - принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель - опишет картинку объектно по аналогии с headerPicture. elem_lt — legacy - элемент-уровневый ключ loadTransparent (если в объекте флаг не задан).""" - if not pic: - return - lt = None - tpx = None - if isinstance(pic, str): - src = pic - else: - src = pic.get('src') - if pic.get('loadTransparent') is not None: - lt = bool(pic.get('loadTransparent')) - tpx = pic.get('transparentPixel') - if not src: - return - if lt is None and elem_lt is not None: - lt = bool(elem_lt) - src_str = str(src) - lines.append(f'{indent}') - if src_str.startswith('abs:'): - lines.append(f'{indent}\t{esc_xml(src_str[4:])}') - else: - lines.append(f'{indent}\t{esc_xml(src_str)}') - lines.append(f'{indent}\t{"false" if lt is False else "true"}') - if tpx: - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# --- Оформление элемента: цвета / шрифты / граница (зеркало form-compile.ps1 Emit-Appearance) --- -# Прямые свойства элемента (// + header/footer у полей). Ключи англ. -# camelCase 1:1 с тегами + приём рус. синонимов. Цвет — verbatim-строка (style:/web:/win:/#RRGGBB); -# шрифт — строка-ref/объект-атрибуты; граница — строка-ref/ -# объект {width,style}. Порядок тегов — XSD (профиль по базовому типу). -APPEARANCE_SPEC = { - 'titleTextColor': ('TitleTextColor', 'color'), - 'titleBackColor': ('TitleBackColor', 'color'), - 'titleFont': ('TitleFont', 'font'), - 'footerTextColor': ('FooterTextColor', 'color'), - 'footerBackColor': ('FooterBackColor', 'color'), - 'footerFont': ('FooterFont', 'font'), - 'textColor': ('TextColor', 'color'), - 'backColor': ('BackColor', 'color'), - 'borderColor': ('BorderColor', 'color'), - 'border': ('Border', 'border'), - 'font': ('Font', 'font'), -} -APPEARANCE_SYNONYMS = { - 'цветтекста': 'textColor', 'цветфона': 'backColor', 'цветрамки': 'borderColor', - 'цветтекстазаголовка': 'titleTextColor', 'цветфоназаголовка': 'titleBackColor', 'шрифтзаголовка': 'titleFont', - 'цветтекстаподвала': 'footerTextColor', 'цветфонаподвала': 'footerBackColor', 'шрифтподвала': 'footerFont', - 'шрифт': 'font', 'рамка': 'border', -} -# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. -# Ключи нормализованы (lowercase, без пробелов); сопоставление в emit_element тоже. Англ. ключ -# работает всегда (доп. слой прощающего ввода). Видимость/Доступность НЕ включаем (hidden/disabled инвертирован). -PROP_SYNONYMS = { - 'пометка': 'checked', - 'кнопкавыбора': 'choiceButton', 'кнопкаочистки': 'clearButton', 'кнопкарегулирования': 'spinButton', - 'кнопкавыпадающегосписка': 'dropListButton', 'кнопкасписковоговыбора': 'choiceListButton', - 'кнопкаоткрытия': 'openButton', 'кнопкапоумолчанию': 'defaultButton', - 'быстрыйвыбор': 'quickChoice', 'формавыбора': 'choiceForm', 'историявыборапривводе': 'choiceHistoryOnInput', - 'выборгруппиэлементов': 'choiceFoldersAndItems', 'фиксациявтаблице': 'fixingInTable', - 'путькданнымподвала': 'footerDataPath', 'автоотметканезаполненного': 'markIncomplete', - 'многострочныйрежим': 'multiLine', 'режимпароля': 'passwordMode', 'переноспословам': 'wrap', - 'расположениезаголовка': 'titleLocation', 'пропускатьпривводе': 'skipOnInput', - 'заголовок': 'title', 'ширина': 'width', 'высота': 'height', 'подсказкаввода': 'inputHint', -} -APP_ORDER_FIELD =['titleTextColor', 'titleBackColor', 'titleFont', 'footerTextColor', 'footerBackColor', 'footerFont', 'textColor', 'backColor', 'borderColor', 'border', 'font'] -APP_ORDER_DECORATION = ['textColor', 'font', 'backColor', 'borderColor', 'border'] -APP_ORDER_BUTTON = ['textColor', 'backColor', 'borderColor', 'font'] - - -def get_appearance_value(el, canonical): - if not isinstance(el, dict): - return None - if canonical in el: - return el[canonical] - lowmap = {k.lower(): k for k in el.keys()} - if canonical.lower() in lowmap: - return el[lowmap[canonical.lower()]] - for syn, canon in APPEARANCE_SYNONYMS.items(): - if canon == canonical and syn in lowmap: - return el[lowmap[syn]] - return None - - -def emit_font_tag(lines, tag, val, indent): - if isinstance(val, str): - lines.append(f'{indent}<{tag} ref="{esc_xml(val)}" kind="StyleItem"/>') - return - attrs = [] - for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): - if a in val and val[a] is not None: - v = val[a] - if isinstance(v, bool): - v = 'true' if v else 'false' - attrs.append(f'{a}="{esc_xml(str(v))}"') - lines.append(f'{indent}<{tag} {" ".join(attrs)}/>') - - -def emit_border_tag(lines, val, indent): - if isinstance(val, str): - lines.append(f'{indent}') - return - if val.get('ref'): - lines.append(f'{indent}') - return - width = val['width'] if val.get('width') is not None else 1 - style = str(val['style']) if 'style' in val else None - lines.append(f'{indent}') - if style: - lines.append(f'{indent}\t{esc_xml(style)}') - lines.append(f'{indent}') - - -# ───────────────────────────────────────────────────────────────────────────── -# Planner design-time — зеркало Emit-PlannerSettings (ps1). -PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' -CHART_NS = 'http://v8.1c.ru/8.2/data/chart' - - -def _pl_get(o, k, default=None): - if isinstance(o, dict) and o.get(k) is not None: - return o[k] - return default - - -def _pl_bool(v): - if isinstance(v, bool): - return 'true' if v else 'false' - if str(v) == 'True': - return 'true' - if str(v) == 'False': - return 'false' - return str(v) - - -def emit_planner_color(lines, tag, o, key, ind): - lines.append(f'{ind}{esc_xml(str(_pl_get(o, key, "auto")))}') - - -def emit_planner_text(lines, tag, v, ind): - if v is None or str(v) == '': - lines.append(f'{ind}') - else: - lines.append(f'{ind}{esc_xml(str(v))}') - - -_PLANNER_REF_RE = re.compile( - r'^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' - r'|\.EnumValue\.|EmptyRef$' - r'|^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') - - -def test_planner_ref(v): - return bool(_PLANNER_REF_RE.search(str(v))) - - -def emit_planner_value(lines, v, ind): - if v is None or str(v) == '': - lines.append(f'{ind}') - return - t = 'xr:DesignTimeRef' if test_planner_ref(v) else 'xs:string' - lines.append(f'{ind}{esc_xml(str(v))}') - - -def emit_planner_font(lines, o, ind): - f = _pl_get(o, 'font') - if f is None: - lines.append(f'{ind}') - return - emit_font_tag(lines, 'pl:font', f, ind) - - -def emit_planner_border(lines, o, ind, key='border'): - b = _pl_get(o, key) - bw = _pl_get(b, 'width', 1) if b else 1 - bs = _pl_get(b, 'style', 'Single') if b else 'Single' - lines.append(f'{ind}') - lines.append(f'{ind}\t{esc_xml(str(bs))}') - lines.append(f'{ind}') - - -def emit_planner_level(lines, lv, cns, ind): - li = f'{ind}\t' - lines.append(f'{ind}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "measure", "Hour")))}') - lines.append(f'{li}{_pl_get(lv, "interval", 1)}') - lines.append(f'{li}{_pl_bool(_pl_get(lv, "show", True))}') - line = _pl_get(lv, 'line') - lw = _pl_get(line, 'width', 1) if line else 1 - lg = _pl_get(line, 'gap', False) if line else False - lst = _pl_get(line, 'style', 'Solid') if line else 'Solid' - lines.append(f'{li}') - lines.append(f'{li}\t{esc_xml(str(lst))}') - lines.append(f'{li}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "scaleColor", "auto")))}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "dayFormatRule", "MonthDayWeekDay")))}') - fmt = _pl_get(lv, 'format') - if fmt is None: - fmt = {'#': 'DF="HH:mm"', 'ru': 'DF="HH:mm"'} - lines.append(f'{li}') - emit_ml_items(lines, f'{li}\t', fmt) - lines.append(f'{li}') - labels = _pl_get(lv, 'labels') - ticks = _pl_get(labels, 'ticks', 0) if labels else 0 - lines.append(f'{li}') - lines.append(f'{li}\t{ticks}') - lines.append(f'{li}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "backColor", "auto")))}') - lines.append(f'{li}{esc_xml(str(_pl_get(lv, "textColor", "auto")))}') - lines.append(f'{li}{_pl_bool(_pl_get(lv, "showPereodicalLabels", True))}') - lines.append(f'{ind}') - - -def emit_planner_timescale(lines, ts, ind): - cns = CHART_NS - ci = f'{ind}\t' - lines.append(f'{ind}') - placement = _pl_get(ts, 'placement', 'Left') if ts else 'Left' - lines.append(f'{ci}{esc_xml(str(placement))}') - levels = _pl_get(ts, 'levels', []) if ts else [] - if not levels: - levels = [None] - for lv in levels: - emit_planner_level(lines, lv, cns, ci) - transp = _pl_get(ts, 'transparent', False) if ts else False - lines.append(f'{ci}{_pl_bool(transp)}') - tbc = _pl_get(ts, 'backColor', 'auto') if ts else 'auto' - ttc = _pl_get(ts, 'textColor', 'auto') if ts else 'auto' - tcl = _pl_get(ts, 'currentLevel', 0) if ts else 0 - lines.append(f'{ci}{esc_xml(str(tbc))}') - lines.append(f'{ci}{esc_xml(str(ttc))}') - lines.append(f'{ci}{tcl}') - lines.append(f'{ind}') - - -def emit_planner_item(lines, it, ind): - lines.append(f'{ind}') - ii = f'{ind}\t' - emit_planner_value(lines, _pl_get(it, 'value'), ii) - emit_planner_text(lines, 'text', _pl_get(it, 'text', ''), ii) - emit_planner_text(lines, 'tooltip', _pl_get(it, 'tooltip', ''), ii) - lines.append(f'{ii}{_pl_get(it, "begin", "0001-01-01T00:00:00")}') - lines.append(f'{ii}{_pl_get(it, "end", "0001-01-01T00:00:00")}') - emit_planner_color(lines, 'borderColor', it, 'borderColor', ii) - emit_planner_color(lines, 'backColor', it, 'backColor', ii) - emit_planner_color(lines, 'textColor', it, 'textColor', ii) - emit_planner_font(lines, it, ii) - lines.append(f'{ii}') - lines.append(f'{ii}{_pl_get(it, "replacementDate", "0001-01-01T00:00:00")}') - lines.append(f'{ii}{_pl_bool(_pl_get(it, "deleted", False))}') - iid = _pl_get(it, 'id') - if iid is None: - import uuid - iid = str(uuid.uuid4()) - lines.append(f'{ii}{iid}') - lines.append(f'{ii}{_pl_bool(_pl_get(it, "textFormatted", False))}') - emit_planner_border(lines, it, ii, 'border') - lines.append(f'{ii}{esc_xml(str(_pl_get(it, "editMode", "EnableEdit")))}') - lines.append(f'{ind}') - - -def emit_planner_dim_element(lines, el, ind): - lines.append(f'{ind}') - ii = f'{ind}\t' - emit_planner_value(lines, _pl_get(el, 'value'), ii) - emit_planner_text(lines, 'text', _pl_get(el, 'text', ''), ii) - emit_planner_color(lines, 'borderColor', el, 'borderColor', ii) - emit_planner_color(lines, 'backColor', el, 'backColor', ii) - emit_planner_color(lines, 'textColor', el, 'textColor', ii) - emit_planner_font(lines, el, ii) - for sub in _pl_get(el, 'elements', []): - emit_planner_dim_element(lines, sub, ii) - lines.append(f'{ii}{_pl_bool(_pl_get(el, "showOnlySubordinatesAreas", True))}') - lines.append(f'{ii}{_pl_bool(_pl_get(el, "textFormatted", False))}') - lines.append(f'{ind}') - - -def emit_planner_dimension(lines, d, ind): - lines.append(f'{ind}') - di = f'{ind}\t' - emit_planner_value(lines, _pl_get(d, 'value'), di) - emit_planner_text(lines, 'text', _pl_get(d, 'text', ''), di) - emit_planner_color(lines, 'borderColor', d, 'borderColor', di) - emit_planner_color(lines, 'backColor', d, 'backColor', di) - emit_planner_color(lines, 'textColor', d, 'textColor', di) - emit_planner_font(lines, d, di) - for el in _pl_get(d, 'elements', []): - emit_planner_dim_element(lines, el, di) - lines.append(f'{di}{_pl_bool(_pl_get(d, "textFormatted", False))}') - lines.append(f'{ind}') - - -def emit_planner_settings(lines, pl, ind): - lines.append(f'{ind}') - si = f'{ind}\t' - for it in _pl_get(pl, 'items', []): - emit_planner_item(lines, it, si) - for d in _pl_get(pl, 'dimensions', []): - emit_planner_dimension(lines, d, si) - emit_planner_color(lines, 'borderColor', pl, 'borderColor', si) - emit_planner_color(lines, 'backColor', pl, 'backColor', si) - emit_planner_color(lines, 'textColor', pl, 'textColor', si) - emit_planner_color(lines, 'lineColor', pl, 'lineColor', si) - emit_planner_font(lines, pl, si) - lines.append(f'{si}{_pl_get(pl, "beginOfRepresentationPeriod", "0001-01-01T00:00:00")}') - lines.append(f'{si}{_pl_get(pl, "endOfRepresentationPeriod", "0001-01-01T00:00:00")}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "alignElementsOfTimeScale", True))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayTimeScaleWrapHeaders", True))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayWrapHeaders", True))}') - wfmt = _pl_get(pl, 'timeScaleWrapHeadersFormat') - if wfmt is None: - wfmt = {'#': 'DLF="DD"', 'ru': 'DLF="DD"'} - emit_mltext(lines, si, 'pl:timeScaleWrapHeadersFormat', wfmt) - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "periodicVariantUnit", "Day")))}') - lines.append(f'{si}{_pl_get(pl, "periodicVariantRepetition", 1)}') - lines.append(f'{si}{_pl_get(pl, "timeScaleWrapBeginIndent", 0)}') - lines.append(f'{si}{_pl_get(pl, "timeScaleWrapEndIndent", 0)}') - emit_planner_timescale(lines, _pl_get(pl, 'timeScale'), si) - period = _pl_get(pl, 'period') - if period: - lines.append(f'{si}') - lines.append(f'{si}\t{_pl_get(period, "begin", "0001-01-01T00:00:00")}') - lines.append(f'{si}\t{_pl_get(period, "end", "0001-01-01T00:00:00")}') - lines.append(f'{si}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayCurrentDate", True))}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsTimeRepresentation", "BeginTime")))}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsBehaviorWhenSpaceInsufficient", "CollapseItems")))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinColumnWidth", True))}') - lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinRowHeight", True))}') - lines.append(f'{si}{_pl_get(pl, "minColumnWidth", 0)}') - lines.append(f'{si}{_pl_get(pl, "minRowHeight", 0)}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixDimensionsHeader", "auto")))}') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixTimeScaleHeader", "auto")))}') - emit_planner_border(lines, pl, si, 'border') - lines.append(f'{si}{esc_xml(str(_pl_get(pl, "newItemsTextType", "String")))}') - lines.append(f'{ind}') - - -# ───────────────────────────────────────────────────────────────────────────── -# Chart design-time — генерик-эмиттер (зеркало -# Build-ChartNode декомпилятора + Emit-ChartNode ps1). -CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'} -CHART_ATTR_FIELDS = {'gaugeQualityBands'} -CHART_FONT_KEYS = ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale') - - -def emit_chart_node(lines, name, val, ind): - if name in CHART_ML_FIELDS: - if val is None or str(val) == '': - lines.append(f'{ind}') - return - lines.append(f'{ind}') - emit_ml_items(lines, f'{ind}\t', val) - lines.append(f'{ind}') - return - if isinstance(val, list): - for e in val: - emit_chart_node(lines, name, e, ind) - return - if isinstance(val, dict): - keys = list(val.keys()) - if name in CHART_ATTR_FIELDS: - attrs = ' '.join(f'{k}="{esc_xml(_pl_bool(val[k]) if isinstance(val[k], bool) else str(val[k]))}"' for k in keys) - lines.append(f'{ind}') - return - if 'gap' in val: - lines.append(f'{ind}') - lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') - lines.append(f'{ind}') - return - if 'style' in val and 'width' in val: - lines.append(f'{ind}') - lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') - lines.append(f'{ind}') - return - if any(fk in val for fk in CHART_FONT_KEYS): - attrs = ' '.join(f'{fk}="{esc_xml(_pl_bool(val[fk]) if isinstance(val[fk], bool) else str(val[fk]))}"' for fk in CHART_FONT_KEYS if fk in val) - lines.append(f'{ind}') - return - if not keys: - lines.append(f'{ind}') - return - lines.append(f'{ind}') - for k in keys: - emit_chart_node(lines, k, val[k], f'{ind}\t') - lines.append(f'{ind}') - return - if val is None or str(val) == '': - lines.append(f'{ind}') - return - if isinstance(val, bool): - lines.append(f'{ind}{_pl_bool(val)}') - return - lines.append(f'{ind}{esc_xml(str(val))}') - - -def emit_chart_settings(lines, chart, ind, ctype='d4p1:Chart'): - lines.append(f'{ind}') - for k in list(chart.keys()): - emit_chart_node(lines, k, chart[k], f'{ind}\t') - lines.append(f'{ind}') - - -def emit_appearance(lines, el, indent, profile='field'): - if not isinstance(el, dict): - return - order = {'decoration': APP_ORDER_DECORATION, 'button': APP_ORDER_BUTTON}.get(profile, APP_ORDER_FIELD) - for key in order: - val = get_appearance_value(el, key) - if val is None or (isinstance(val, str) and val == ''): - continue - tag, kind = APPEARANCE_SPEC[key] - if kind == 'color': - lines.append(f'{indent}<{tag}>{esc_xml(str(val))}') - elif kind == 'font': - emit_font_tag(lines, tag, val, indent) - else: - emit_border_tag(lines, val, indent) - - -# Простые скаляры элемента (pass-through, зеркало $script:genericScalars). kind bool/value. -GENERIC_SCALARS = [ - ('VerticalAlign', 'verticalAlign', 'value'), - ('ThroughAlign', 'throughAlign', 'value'), - ('EnableContentChange', 'enableContentChange', 'bool'), - ('PictureSize', 'pictureSize', 'value'), - ('TitleHeight', 'titleHeight', 'value'), - ('ChildItemsWidth', 'childItemsWidth', 'value'), - ('ShowLeftMargin', 'showLeftMargin', 'bool'), - ('CellHyperlink', 'cellHyperlink', 'bool'), - ('ViewMode', 'viewMode', 'value'), - ('VerticalScrollBar', 'verticalScrollBar', 'value'), - ('RowInputMode', 'rowInputMode', 'value'), - ('Mask', 'mask', 'value'), - ('CreateButton', 'createButton', 'bool'), - ('FixingInTable', 'fixingInTable', 'value'), - ('VerticalSpacing', 'verticalSpacing', 'value'), - # Spec-fields (document/gauge) - type-specific enum/bool scalars pass-through - ('HorizontalScrollBar', 'horizontalScrollBar', 'value'), - ('ViewScalingMode', 'viewScalingMode', 'value'), - ('Output', 'output', 'value'), - ('SelectionShowMode', 'selectionShowMode', 'value'), - ('PointerType', 'pointerType', 'value'), - ('DrawingSelectionShowMode', 'drawingSelectionShowMode', 'value'), - ('WarningOnEditRepresentation', 'warningOnEditRepresentation', 'value'), - ('MarkingAppearance', 'markingAppearance', 'value'), - ('Protection', 'protection', 'bool'), - ('Edit', 'edit', 'bool'), - ('ShowGrid', 'showGrid', 'bool'), - ('ShowGroups', 'showGroups', 'bool'), - ('ShowHeaders', 'showHeaders', 'bool'), - ('ShowRowAndColumnNames', 'showRowAndColumnNames', 'bool'), - ('ShowCellNames', 'showCellNames', 'bool'), - ('ShowPercent', 'showPercent', 'bool'), - # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы - ('HorizontalSpacing', 'horizontalSpacing', 'value'), - ('RepresentationInContextMenu', 'representationInContextMenu', 'value'), - ('SettingsNamedItemDetailedRepresentation', 'settingsNamedItemDetailedRepresentation', 'bool'), - # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) - ('ItemHeight', 'itemHeight', 'value'), - ('DropListWidth', 'dropListWidth', 'value'), - # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам - ('TitleDataPath', 'titleDataPath', 'value'), - ('ExtendedEdit', 'extendedEdit', 'bool'), - ('MaxRowsCount', 'maxRowsCount', 'value'), - ('AutoMaxRowsCount', 'autoMaxRowsCount', 'bool'), - ('HeightControlVariant', 'heightControlVariant', 'value'), - ('EditTextUpdate', 'editTextUpdate', 'value'), - # Корпусный хвост: свёртка группы / форма попапа / авто-добавление / выделение отрицательных / - # нач. позиция списка / высота списка выбора / три состояния / прокрутка страницы при сжатии - ('ControlRepresentation', 'controlRepresentation', 'value'), - ('ShapeRepresentation', 'shapeRepresentation', 'value'), - ('AutoAddIncomplete', 'autoAddIncomplete', 'bool'), - ('MarkNegatives', 'markNegatives', 'bool'), - ('InitialListView', 'initialListView', 'value'), - ('ChoiceListHeight', 'choiceListHeight', 'value'), - ('ThreeState', 'threeState', 'bool'), - ('ScrollOnCompress', 'scrollOnCompress', 'bool'), - # Сочетание клавиш — общее свойство (команда — отдельный путь) - ('Shortcut', 'shortcut', 'value'), - # Батч простых скаляров (input/radio/group/picDecoration/button; Table-специфичные — отдельно) - ('IncompleteChoiceMode', 'incompleteChoiceMode', 'value'), - ('EqualColumnsWidth', 'equalColumnsWidth', 'bool'), - ('ChildrenAlign', 'childrenAlign', 'value'), - ('ImageScale', 'imageScale', 'value'), - ('Zoomable', 'zoomable', 'bool'), - ('Shape', 'shape', 'value'), - ('PictureLocation', 'pictureLocation', 'value'), - # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) - ('EqualItemsWidth', 'equalItemsWidth', 'bool'), - ('ItemTitleHeight', 'itemTitleHeight', 'value'), -] - - -def emit_generic_scalars(lines, el, indent): - for tag, key, kind in GENERIC_SCALARS: - if key not in el or el[key] is None: - continue - if kind == 'bool': - lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') - else: - v = str(el[key]) - if v == '': - continue - lines.append(f'{indent}<{tag}>{esc_xml(v)}') - - -def emit_layout(lines, el, indent, skip_height=False, multi_line_default=False): - # Общие layout-свойства — применимы ко всем элементам. Порядок согласован - # с историческим выводом input/label, чтобы не сдвигать существующие снапшоты. - # skip_height: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). - # multi_line_default: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. - # CommandSet (отключённые команды редактора) — общее свойство поля; в схеме рано (после TitleLocation). - if el.get('excludedCommands') and len(el['excludedCommands']) > 0: - lines.append(f'{indent}') - for cmd in el['excludedCommands']: - lines.append(f'{indent}\t{cmd}') - lines.append(f'{indent}') - emit_common_element_props(lines, el, indent) - if 'autoMaxWidth' in el: - if el.get('autoMaxWidth') is False: - lines.append(f"{indent}false") - elif multi_line_default: - lines.append(f"{indent}false") - if el.get('maxWidth') is not None: - lines.append(f"{indent}{el['maxWidth']}") - if el.get('autoMaxHeight') is False: - lines.append(f"{indent}false") - if el.get('maxHeight') is not None: - lines.append(f"{indent}{el['maxHeight']}") - if el.get('width'): - lines.append(f"{indent}{el['width']}") - if not skip_height and el.get('height'): - lines.append(f"{indent}{el['height']}") - if el.get('horizontalStretch') is not None: - lines.append(f'{indent}{"true" if el["horizontalStretch"] else "false"}') - if el.get('verticalStretch') is not None: - lines.append(f'{indent}{"true" if el["verticalStretch"] else "false"}') - if el.get('groupHorizontalAlign'): - lines.append(f"{indent}{el['groupHorizontalAlign']}") - if el.get('groupVerticalAlign'): - lines.append(f"{indent}{el['groupVerticalAlign']}") - if el.get('horizontalAlign'): - lines.append(f"{indent}{el['horizontalAlign']}") - emit_generic_scalars(lines, el, indent) - - -def title_from_name(name): - """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" - if not name: - return '' - s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) - s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) - parts = s.split(' ') - if not parts: - return s - out = [parts[0]] - for p in parts[1:]: - out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) - return ' '.join(out) - - -def emit_title(lines, el, name, indent, auto=False): - # Нет ключа title → авто-вывод из имени (помощь модели). - # Явный title "" (или None) → подавить. Явный непустой → как есть. - if 'title' in el: - if el.get('title'): - emit_mltext(lines, indent, 'Title', el['title']) - elif auto and name: - emit_mltext(lines, indent, 'Title', title_from_name(name)) - # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. - if el.get('tooltip'): - emit_mltext(lines, indent, 'ToolTip', el['tooltip']) - # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. - if el.get('tooltipRepresentation'): - lines.append(f'{indent}{el["tooltipRepresentation"]}') - - -_TITLE_LOC_MAP = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} - - -def map_title_loc(v): - return _TITLE_LOC_MAP.get(str(v).lower(), str(v)) - - -def emit_title_location(lines, el, indent, smart_default): - # Нет ключа → умный дефолт (Right/None), эмитится. "" → подавить (дефолт платформы). - # Значение → эмитить с маппингом регистра. - if 'titleLocation' in el: - if el.get('titleLocation'): - lines.append(f"{indent}{map_title_loc(el['titleLocation'])}") - elif smart_default: - lines.append(f"{indent}{smart_default}") - - -# --- 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 object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', - '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", - "характеристика": "Characteristic", - "любаяссылка": "AnyRef", - "любаяссылкаиб": "AnyIBRef", - # Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: - "standardperiod": "v8:StandardPeriod", - "стандартныйпериод": "v8:StandardPeriod", - "standardbeginningdate": "v8:StandardBeginningDate", - "стандартнаядатаначала": "v8:StandardBeginningDate", - "uuid": "v8:UUID", - "уникальныйидентификатор": "v8:UUID", - "списокзначений": "ValueList", -} - - -def resolve_type_str(type_str): - if not type_str: - return type_str - # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) - if type_str.startswith('cfg:'): - type_str = type_str[4:] - 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) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) - m = re.match(r'^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str, re.IGNORECASE) - if m: - length = m.group(2) if m.group(2) else '0' - al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable' - lines.append(f'{indent}xs:string') - lines.append(f'{indent}') - lines.append(f'{indent}\t{length}') - lines.append(f'{indent}\t{al}') - 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 - - # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. - # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. - if type_str in ('DynamicList', 'ConstantsSet', 'ReportObject'): - lines.append(f'{indent}cfg:{type_str}') - return - - # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) - # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. - if re.match(r'^(DefinedType|Characteristic)\.', type_str): - lines.append(f'{indent}cfg:{type_str}') - return - if re.match(r'^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$', type_str): - lines.append(f'{indent}cfg:{type_str}') - return - - # cfg: references - if CFG_REF_PATTERN.match(type_str): - lines.append(f'{indent}cfg:{type_str}') - return - - # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). - # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. - # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, - # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. - special_type_ns = { - "mxl:SpreadsheetDocument": "http://v8.1c.ru/8.2/data/spreadsheet", - "fd:FormattedDocument": "http://v8.1c.ru/8.2/data/formatted-document", - "d5p1:TextDocument": "http://v8.1c.ru/8.1/data/txtedt", - "d5p1:Chart": "http://v8.1c.ru/8.2/data/chart", - "d5p1:GanttChart": "http://v8.1c.ru/8.2/data/chart", - "d5p1:Dendrogram": "http://v8.1c.ru/8.2/data/chart", - "d5p1:FlowchartContextType": "http://v8.1c.ru/8.2/data/graphscheme", - "d5p1:DataAnalysisTimeIntervalUnitType": "http://v8.1c.ru/8.2/data/data-analysis", - "d5p1:GeographicalSchema": "http://v8.1c.ru/8.2/data/geo", - "pdfdoc:PDFDocument": "http://v8.1c.ru/8.3/data/pdf", - "pl:Planner": "http://v8.1c.ru/8.3/data/planner", - } - if type_str in special_type_ns: - pref = type_str.split(':', 1)[0] - lines.append(f'{indent}{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]}") - # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — verbatim (напр. v8:UUID, v8:StandardPeriod). - if re.match(r'^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):', type_str): - lines.append(f'{indent}{type_str}') - elif '.' 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, tag="Type", tag_attrs=""): - # tag/tag_attrs — обёртка (по умолчанию ); для valueType ValueList вызывается с - # tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"'. - if not type_str: - lines.append(f'{indent}<{tag}{tag_attrs}/>') - return - - type_string = str(type_str) - parts = [p.strip() for p in re.split(r'[|+]', type_string)] - - lines.append(f'{indent}<{tag}{tag_attrs}>') - for part in parts: - emit_single_type(lines, part, f'{indent}\t') - lines.append(f'{indent}') - - -# --- Element emitters --- - -def emit_element(lines, el, indent, in_cmd_bar=False): - # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. - normalize_panel_synonyms(el) - - # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). - # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя). - for src, dst in ELEMENT_TYPE_SYNONYMS.items(): - if src in el and dst not in el: - if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): - continue - el[dst] = el.pop(src) - - # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. - # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. - for p_name in list(el.keys()): - norm = p_name.replace(' ', '').lower() - canon = PROP_SYNONYMS.get(norm) - if canon and p_name != canon: - val = el.pop(p_name) - if canon not in el: - el[canon] = val - - 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 (внутренние маркеры на _ пропускаем). Оформление (цвета/шрифты/граница) - # проверяем против самих структур appearance — канонические ключи + forgiving-синонимы, чтобы - # allowlist не дрейфовал при добавлении новых. - for p_name in el.keys(): - if p_name.startswith('_'): - continue - if p_name not in KNOWN_KEYS and p_name not in APPEARANCE_SPEC and p_name not in APPEARANCE_SYNONYMS: - 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) - _ensure_unique(name, _seen_element_names, 'element') - eid = new_id() - - emitters = { - 'group': emit_group, - 'columnGroup': emit_column_group, - 'buttonGroup': emit_button_group, - 'input': emit_input, - 'check': emit_check, - 'radio': emit_radio_button_field, - '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, - 'searchString': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchString', indent), - 'viewStatus': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'viewStatus', indent), - 'searchControl': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchControl', indent), - 'spreadsheet': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'SpreadSheetDocumentField', 'spreadsheet'), - 'html': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'HTMLDocumentField', 'html'), - 'textDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TextDocumentField', 'textDoc'), - 'formattedDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'FormattedDocumentField', 'formattedDoc'), - 'progressBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ProgressBarField', 'progressBar'), - 'trackBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TrackBarField', 'trackBar'), - 'chart': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ChartField', 'chart'), - 'graphicalSchema': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'GraphicalSchemaField', 'graphicalSchema'), - 'planner': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PlannerField', 'planner'), - 'periodField': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PeriodField', 'periodField'), - 'dendrogram': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'DendrogramField', 'dendrogram'), - 'ganttChart': emit_gantt_chart, - } - - emitter = emitters.get(type_key) - if emitter: - if type_key == 'button': - emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) - else: - 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 orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. - group_val = str(el.get('group', '')).lower() - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'alwayshorizontal': 'AlwaysHorizontal', - 'alwaysvertical': 'AlwaysVertical', - 'horizontalifpossible': 'HorizontalIfPossible', - 'collapsible': 'Vertical', - } - orientation = orientation_map.get(group_val) - if orientation: - lines.append(f'{inner}{orientation}') - - # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). - behavior_val = str(el['behavior']).lower() if el.get('behavior') else ('collapsible' if group_val == 'collapsible' else None) - bmap = {'usual': 'Usual', 'collapsible': 'Collapsible', 'popup': 'PopUp'} - if behavior_val and behavior_val in bmap: - lines.append(f'{inner}{bmap[behavior_val]}') - # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) - if el.get('collapsed') is True: - lines.append(f'{inner}true') - - # 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 not None: - lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') - # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст - if el.get('collapsedTitle'): - emit_mltext(lines, inner, 'CollapsedRepresentationTitle', el['collapsedTitle']) - - # United - if el.get('united') is False: - lines.append(f'{inner}false') - - # Формат значения пути к данным заголовка (; парный к titleDataPath группы) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - - # Оформление (цвета/шрифты/граница) — перед компаньоном - emit_appearance(lines, el, inner, 'field') - - # 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, el.get('extendedTooltip')) - - # 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_column_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - group_val = str(el.get('columnGroup', '')) - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'inCell': 'InCell', - } - orientation = orientation_map.get(group_val) - if orientation: - lines.append(f'{inner}{orientation}') - - if el.get('showTitle') is not None: - lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') - # showInHeader эмитится общим emit_common_element_props (через emit_layout) - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - - # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) - emit_column_pics(lines, el, inner) - - # Оформление (цвета/шрифты/граница) — перед компаньоном - emit_appearance(lines, el, inner, 'field') - - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - 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, auto=not el.get('path')) - 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 not None: - lines.append(f'{inner}{"true" if el["multiLine"] else "false"}') - if el.get('passwordMode') is not None: - lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') - # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, - # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). - if el.get('choiceButton') is not None: - lines.append(f'{inner}{"true" if el["choiceButton"] else "false"}') - # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) - if el.get('clearButton') is not None: - lines.append(f'{inner}{"true" if el["clearButton"] else "false"}') - if el.get('spinButton') is not None: - lines.append(f'{inner}{"true" if el["spinButton"] else "false"}') - if el.get('dropListButton') is not None: - lines.append(f'{inner}{"true" if el["dropListButton"] else "false"}') - if el.get('choiceListButton') is not None: - lines.append(f'{inner}{"true" if el["choiceListButton"] else "false"}') - if el.get('markIncomplete') is not None: - lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_column_pics(lines, el, inner) - if el.get('textEdit') is False: - lines.append(f'{inner}false') - # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) - for key, tag in (('wrap', 'Wrap'), ('openButton', 'OpenButton'), ('listChoiceMode', 'ListChoiceMode'), - ('extendedEditMultipleValues', 'ExtendedEditMultipleValues'), ('chooseType', 'ChooseType'), - ('quickChoice', 'QuickChoice'), ('autoChoiceIncomplete', 'AutoChoiceIncomplete')): - if el.get(key) is not None: - lines.append(f'{inner}<{tag}>{"true" if el[key] else "false"}') - # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. - # availableTypes — формат типа реквизита (§type); emit_type сам разбирает мультитип "a | b". - if el.get('typeDomainEnabled') is not None: - lines.append(f'{inner}{"true" if el["typeDomainEnabled"] else "false"}') - if el.get('availableTypes'): - emit_type(lines, el['availableTypes'], inner, tag='AvailableTypes') - # InputField-специфичные value-скаляры - for key, tag in (('choiceForm', 'ChoiceForm'), ('choiceHistoryOnInput', 'ChoiceHistoryOnInput'), - ('choiceFoldersAndItems', 'ChoiceFoldersAndItems'), ('footerDataPath', 'FooterDataPath')): - if el.get(key): - lines.append(f'{inner}<{tag}>{esc_xml(str(el[key]))}') - # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). - for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue')): - if el.get(key) is not None: - mvt = 'xs:string' if isinstance(el[key], str) else 'xs:decimal' - lines.append(f'{inner}<{tag} xsi:type="{mvt}">{esc_xml(str(el[key]))}') - if el.get('choiceButtonRepresentation'): - lines.append(f'{inner}{el["choiceButtonRepresentation"]}') - emit_picture_ref(lines, el.get('choiceButtonPicture'), 'ChoiceButtonPicture', inner) - emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) - - if el.get('inputHint'): - emit_mltext(lines, inner, 'InputHint', el['inputHint']) - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - if el.get('footerText') is not None: - emit_mltext(lines, inner, 'FooterText', el['footerText']) - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - emit_choice_list(lines, el, inner) - - # Связи по типу / связи параметров выбора / параметры выбора - emit_type_link(lines, el, inner) - emit_choice_parameter_links(lines, el, inner) - emit_choice_parameters(lines, el, inner) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - 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, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_column_pics(lines, el, inner) - # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг - _cbt_map = {'auto': 'Auto', 'checkbox': 'CheckBox', 'switcher': 'Switcher', 'tumbler': 'Tumbler'} - if 'checkBoxType' in el: - if el.get('checkBoxType'): - lines.append(f'{inner}{_cbt_map.get(str(el["checkBoxType"]).lower(), el["checkBoxType"])}') - else: - lines.append(f'{inner}Auto') - - emit_title_location(lines, el, inner, 'Right') - - emit_layout(lines, el, inner) - - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'check') - - lines.append(f'{indent}') - - -def emit_radio_button_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, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_title_location(lines, el, inner, 'None') - - rbt = normalize_radio_button_type(el.get('radioButtonType')) - lines.append(f'{inner}{rbt}') - - if el.get('columnsCount') is not None: - lines.append(f'{inner}{el["columnsCount"]}') - - emit_choice_list(lines, el, inner) - - emit_layout(lines, el, inner) - - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'radio') - - lines.append(f'{indent}') - - -# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму -# (reuse resolve_ml_formatted, как у extendedTooltip). Sibling-ключ formatted — back-compat override. -def emit_decoration_title(lines, el, name, indent, auto=False): - has_key = 'title' in el - title_val = el['title'] if has_key else (title_from_name(name) if (auto and name) else None) - if title_val: - text, fmt = resolve_ml_formatted(title_val) - if 'formatted' in el: - fmt = bool(el['formatted']) - lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') - emit_ml_items(lines, f'{indent}\t', text) - lines.append(f'{indent}') - if el.get('tooltip'): - emit_mltext(lines, indent, 'ToolTip', el['tooltip']) - if el.get('tooltipRepresentation'): - lines.append(f'{indent}{el["tooltipRepresentation"]}') - - -def emit_label(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title - # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). - emit_common_flags(lines, el, inner) - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, el, inner) - emit_appearance(lines, el, inner, 'decoration') - - emit_decoration_title(lines, el, name, inner, auto=True) - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - 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, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode - if el.get('footerDataPath'): - lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') - # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение - if el.get('passwordMode') is not None: - lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') - emit_column_pics(lines, el, inner) - # ВНИМАНИЕ: у LabelField платформенный тег (опечатка 1С), не . - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, el, inner) - - if el.get('warningOnEdit') is not None: - emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) - if el.get('footerText') is not None: - emit_mltext(lines, inner, 'FooterText', el['footerText']) - - # Формат / формат редактирования (LocalStringType — строка или {ru,en}) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - - # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'labelField') - - lines.append(f'{indent}') - - -# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). -def emit_dynlist_table_block(lines, el, indent): - # (useAlternationRowColor — общее свойство таблицы, эмитится в emit_table) - # Group A (гарант. блок): дефолт + override - ar = 'true' if el.get('autoRefresh') is True else 'false' - lines.append(f'{indent}{ar}') - arp = el['autoRefreshPeriod'] if el.get('autoRefreshPeriod') is not None else 60 - lines.append(f'{indent}{arp}') - lines.append(f'{indent}') - lines.append(f'{indent}\tCustom') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}') - cfi = el.get('choiceFoldersAndItems') or 'Items' - lines.append(f'{indent}{cfi}') - rcr = 'true' if el.get('restoreCurrentRow') is True else 'false' - lines.append(f'{indent}{rcr}') - lines.append(f'{indent}') - sr = 'false' if el.get('showRoot') is False else 'true' - lines.append(f'{indent}{sr}') - arc = 'true' if el.get('allowRootChoice') is True else 'false' - lines.append(f'{indent}{arc}') - uodc = el.get('updateOnDataChange') or 'Auto' - lines.append(f'{indent}{uodc}') - if el.get('userSettingsGroup'): - lines.append(f'{indent}{el["userSettingsGroup"]}') - agcru = 'false' if el.get('allowGettingCurrentRowURL') is False else 'true' - lines.append(f'{indent}{agcru}') - - -def emit_table(lines, el, name, eid, indent): - _current_table_name['name'] = name # дефолт source для кастомных дополнений в commandBar - lines.append(f'{indent}
') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - # ChangeRowSet/Order — явное значение (в т.ч. false: платформа пишет его на ValueTable) - if 'changeRowSet' in el and el['changeRowSet'] is not None: - lines.append(f'{inner}{"true" if el["changeRowSet"] is True else "false"}') - if 'changeRowOrder' in el and el['changeRowOrder'] is not None: - lines.append(f'{inner}{"true" if el["changeRowOrder"] is True else "false"}') - if el.get('autoInsertNewRow') is True: - lines.append(f'{inner}true') - # RowFilter — nil-плейсхолдер (ключ присутствует → эмитим) - if 'rowFilter' in el: - lines.append(f'{inner}') - # Высота в строках () — отдельное свойство от (высота элемента, - # эмитится generic-ом emit_layout ниже). Таблица может нести оба (237 в корпусе). - if el.get('heightInTableRows'): - lines.append(f'{inner}{el["heightInTableRows"]}') - 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') - # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). - if el.get('autofill') is not None: - lines.append(f'{inner}{"true" if el["autofill"] else "false"}') - if el.get('multipleChoice') is True: - lines.append(f'{inner}true') - if el.get('searchOnInput'): - lines.append(f'{inner}{el["searchOnInput"]}') - if el.get('markIncomplete') is not None: - lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') - # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) - if el.get('headerHeight') is not None: - lines.append(f'{inner}{el["headerHeight"]}') - if el.get('footerHeight') is not None: - lines.append(f'{inner}{el["footerHeight"]}') - if el.get('useAlternationRowColor') is True: - lines.append(f'{inner}true') - if el.get('selectionMode'): - lines.append(f'{inner}{el["selectionMode"]}') - if el.get('rowSelectionMode'): - lines.append(f'{inner}{el["rowSelectionMode"]}') - if el.get('verticalLines') is False: - lines.append(f'{inner}false') - if el.get('horizontalLines') is False: - lines.append(f'{inner}false') - if el.get('initialTreeView'): - lines.append(f'{inner}{el["initialTreeView"]}') - if el.get('enableDrag') is not None: - lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') - if el.get('rowPictureDataPath'): - lines.append(f'{inner}{el["rowPictureDataPath"]}') - # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) - emit_picture_ref(lines, el.get('rowsPicture'), 'RowsPicture', inner) - # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) - if el.get('currentRowUse'): - lines.append(f'{inner}{el["currentRowUse"]}') - # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) - if el.get('refreshRequest'): - lines.append(f'{inner}{el["refreshRequest"]}') - # Блок свойств дин-список-таблицы (помечена эвристикой) - if el.get('_dynList'): - emit_dynlist_table_block(lines, el, inner) - if el.get('viewStatusLocation'): - lines.append(f'{inner}{el["viewStatusLocation"]}') - if el.get('searchControlLocation'): - lines.append(f'{inner}{el["searchControlLocation"]}') - emit_layout(lines, el, inner) - - # CommandSet таблицы эмитится через emit_layout (общий механизм поля) - - # Оформление (цвета/граница таблицы) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - # AutoCommandBar — with optional Autofill control - if el.get('commandBar') is not None: - emit_companion_panel(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner, el.get('commandBar')) - elif 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, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) - adds = el.get('additions') - emit_table_addition(lines, 'searchString', name, inner, get_addition_override(adds, 'searchString')) - emit_table_addition(lines, 'viewStatus', name, inner, get_addition_override(adds, 'viewStatus')) - emit_table_addition(lines, 'searchControl', name, inner, get_addition_override(adds, 'searchControl')) - - # 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' - - emit_title(lines, el, name, inner) - - if el.get('pagesRepresentation'): - lines.append(f'{inner}{el["pagesRepresentation"]}') - - emit_common_flags(lines, el, inner) - emit_layout(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, el.get('extendedTooltip')) - - 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, auto=True) - emit_common_flags(lines, el, inner) - - # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). - # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. - emit_picture_ref(lines, el.get('picture'), 'Picture', inner) - - if el.get('group'): - orientation_map = { - 'horizontal': 'Horizontal', - 'vertical': 'Vertical', - 'alwaysHorizontal': 'AlwaysHorizontal', - 'alwaysVertical': 'AlwaysVertical', - 'horizontalIfPossible': 'HorizontalIfPossible', - } - orientation = orientation_map.get(str(el['group'])) - if orientation: - lines.append(f'{inner}{orientation}') - if el.get('showTitle') is not None: - lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') - # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) - if el.get('format'): - emit_mltext(lines, inner, 'Format', el['format']) - if el.get('editFormat'): - emit_mltext(lines, inner, 'EditFormat', el['editFormat']) - emit_layout(lines, el, inner) - - # \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (BackColor / TitleTextColor / TitleFont) \u2014 \u043f\u043e\u0441\u043b\u0435 ShowTitle, \u043f\u0435\u0440\u0435\u0434 \u043a\u043e\u043c\u043f\u0430\u043d\u044c\u043e\u043d\u043e\u043c - emit_appearance(lines, el, inner, 'field') - - # 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, el.get('extendedTooltip')) - - # 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, in_cmd_bar=False): - lines.append(f'{indent}') - - -def emit_picture_decoration(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_decoration_title(lines, el, name, inner) - # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) - if el.get('nonselectedPictureText') is not None: - emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) - emit_common_flags(lines, el, inner) - - # Источник картинки — ТОЛЬКО src (ключ 'picture' = тип/имя элемента, не источник). - # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . - if el.get('src'): - src_str = str(el['src']) - lt = 'true' if el.get('loadTransparent') is True else 'false' - lines.append(f'{inner}') - if src_str.startswith('abs:'): - lines.append(f'{inner}\t{esc_xml(src_str[4:])}') - else: - lines.append(f'{inner}\t{esc_xml(src_str)}') - lines.append(f'{inner}\t{lt}') - tpx = el.get('transparentPixel') - if tpx: - lines.append(f'{inner}\t') - lines.append(f'{inner}') - - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - emit_layout(lines, el, inner) - - # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) - emit_appearance(lines, el, inner, 'decoration') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - 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('editMode'): - lines.append(f'{inner}{el["editMode"]}') - emit_column_pics(lines, el, inner) - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - if el.get('hyperlink') is True: - lines.append(f'{inner}true') - - emit_layout(lines, el, inner) - - # ValuesPicture — picture (collection) used to render the field's value. - # Required for a Boolean-bound PictureField to actually show an icon. - # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. - emit_picture_ref(lines, el.get('valuesPicture'), 'ValuesPicture', inner) - if el.get('nonselectedPictureText') is not None: - emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, 'picField') - - lines.append(f'{indent}') - - -def emit_simple_field(lines, el, name, eid, indent, xml_tag, type_key): - # Спец-поля "документ/датчик" (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): - # единый скелет поля. Типоспец. enum/bool скаляры — через generic (emit_layout); - # числовые скаляры датчиков (min/max/шаги) — без xsi:type; enableDrag — фактическое значение. - lines.append(f'{indent}<{xml_tag} name="{name}" id="{eid}"{di_attr(el)}>') - inner = f'{indent}\t' - - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - if el.get('editMode'): - lines.append(f'{inner}{el["editMode"]}') - - emit_layout(lines, el, inner) - - # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через emit_layout. - if el.get('enableDrag') is not None: - lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') - - # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) - for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue'), ('largeStep', 'LargeStep'), ('markingStep', 'MarkingStep'), ('step', 'Step')): - if el.get(key) is not None: - lines.append(f'{inner}<{tag}>{el[key]}') - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - emit_events(lines, el, name, inner, type_key) - - lines.append(f'{indent}') - - -def emit_gantt_chart(lines, el, name, eid, indent): - # GanttChartField — скелет поля + вложенная (полноценная таблица, через emit_element). - lines.append(f'{indent}') - inner = f'{indent}\t' - if el.get('path'): - lines.append(f'{inner}{el["path"]}') - emit_title(lines, el, name, inner, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - if el.get('titleLocation'): - lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') - emit_layout(lines, el, inner) - emit_appearance(lines, el, inner, 'field') - emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем emit_element) - if el.get('ganttTable'): - emit_element(lines, el['ganttTable'], inner) - emit_events(lines, el, name, inner, 'ganttChart') - 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, auto=not el.get('path')) - emit_common_flags(lines, el, inner) - - if el.get('titleLocation'): - loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} - loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) - lines.append(f'{inner}{loc}') - - emit_layout(lines, el, inner) - - # Календарно-специфичные свойства (порядок схемы: после layout, до companions) - if el.get('selectionMode'): - lines.append(f'{inner}{el["selectionMode"]}') - if el.get('showCurrentDate') is not None: - lines.append(f'{inner}{"true" if el["showCurrentDate"] else "false"}') - if el.get('widthInMonths') is not None: - lines.append(f'{inner}{el["widthInMonths"]}') - if el.get('heightInMonths') is not None: - lines.append(f'{inner}{el["heightInMonths"]}') - if el.get('showMonthsPanel') is not None: - lines.append(f'{inner}{"true" if el["showMonthsPanel"] else "false"}') - - # Оформление (цвета/шрифты/граница) — перед компаньонами - emit_appearance(lines, el, inner, 'field') - - # Companions - emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) - 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, el.get('extendedTooltip')) - - 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' - - emit_title(lines, el, name, inner) - - if el.get('commandSource'): - lines.append(f'{inner}{el["commandSource"]}') - - if el.get('autofill') is True: - lines.append(f'{inner}true') - - _hl = get_hlocation(el) - if _hl: - lines.append(f'{inner}{_hl}') - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - # 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', in_cmd_bar=True) - 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, auto=True) - emit_common_flags(lines, el, inner) - - emit_command_picture(lines, el.get('picture'), el.get('loadTransparent'), inner) - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - emit_layout(lines, el, inner) - - # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном - emit_appearance(lines, el, inner, 'field') - - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - # 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', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -def emit_button_group(lines, el, name, eid, indent): - lines.append(f'{indent}') - inner = f'{indent}\t' - - emit_title(lines, el, name, inner) - - if el.get('commandSource'): - lines.append(f'{inner}{el["commandSource"]}') - - if el.get('representation'): - lines.append(f'{inner}{el["representation"]}') - - emit_common_flags(lines, el, inner) - emit_layout(lines, el, inner) - - # Companion: ExtendedTooltip - emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) - - # 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', in_cmd_bar=True) - lines.append(f'{inner}') - - lines.append(f'{indent}') - - -# --- Attribute emitter --- - -def emit_functional_options(lines, fo, indent): - # FunctionalOption.X…> — у Attribute/Command/Column. - # Forgiving: "X"/"FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. - if not fo: - return - lines.append(f'{indent}') - for opt in fo: - v = str(opt) - if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$', v): - pass - elif v.startswith('FunctionalOption.'): - pass - else: - v = f'FunctionalOption.{v}' - lines.append(f'{indent}\t{v}') - lines.append(f'{indent}') - - -def emit_attr_column(lines, col, indent): - # Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. - col_id = new_id() - lines.append(f'{indent}') - if col.get('title'): - emit_mltext(lines, f'{indent}\t', 'Title', col['title']) - emit_type(lines, str(col.get('type', '')), f'{indent}\t') - emit_functional_options(lines, col.get('functionalOptions'), f'{indent}\t') - # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита - if col.get('view') is not None: - emit_xr_flag(lines, 'View', col['view'], f'{indent}\t') - if col.get('edit') is not None: - emit_xr_flag(lines, 'Edit', col['edit'], f'{indent}\t') - lines.append(f'{indent}') - - -# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- -# Зеркало form-compile.ps1 (Emit-DLParameters). Та же сущность, что параметры СКД, но в -# форме: обёртка + дети dcssch:. DSL переиспользует грамматику параметров СКД. -# Контекстные дефолты: useRestriction эмитим ВСЕГДА, дефолт true (в СКД false); title — авто -# из имени; пустое value — всегда xsi:nil (даже при известном типе). Канон. порядок детей -# (по корпусу): name, title, valueType, value, useRestriction, expression, availableValue*, -# valueListAllowed, availableAsField, inputParameters, denyIncompleteValues, use. - -def emit_dl_mltext(lines, indent, tag, text): - # ML-текст с xsi:type="v8:LocalStringType" (в dcssch:* обязателен; emit_mltext его не ставит). - lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">') - emit_ml_items(lines, f'{indent}\t', text) - lines.append(f'{indent}') - - -def split_dl_valuelist_csv(s): - result = [] - if s is None: - return result - items = [] - buf = [] - in_quote = None - for ch in s: - if in_quote: - buf.append(ch) - if ch == in_quote: - in_quote = None - elif ch in ("'", '"'): - in_quote = ch - buf.append(ch) - elif ch == ',': - items.append(''.join(buf)); buf = [] - else: - buf.append(ch) - if buf: - items.append(''.join(buf)) - for raw in items: - t = raw.strip() - if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')): - t = t[1:-1] - if t != '': - result.append(t) - return result - - -def parse_dl_param_shorthand(s): - result = {'name': '', 'type': '', 'value': None, 'title': None} - if '@valueList' in s: - result['valueListAllowed'] = True - s = re.sub(r'\s*@valueList', '', s) - if '@hidden' in s: - result['hidden'] = True - s = re.sub(r'\s*@hidden', '', s) - m = re.search(r'\[([^\]]*)\]', s) - if m: - result['title'] = m.group(1).strip() - s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip() - # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). - m = re.match(r'^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$', s) - if m: - result['name'] = m.group(1).strip() - type_raw = m.group(2).strip() - if re.search(r'[|+]', type_raw): - result['type'] = ' | '.join(resolve_type_str(p.strip()) for p in re.split(r'\s*[|+]\s*', type_raw)) - else: - result['type'] = resolve_type_str(type_raw) - if m.group(4): - rhs = m.group(4).strip() - items = split_dl_valuelist_csv(rhs) - if len(items) >= 2: - result['value'] = items - result['valueListAllowed'] = True - elif len(items) == 1: - result['value'] = items[0] - else: - result['value'] = rhs - else: - result['name'] = s.strip() - return result - - -def is_dl_empty_value(v): - if v is None: - return True - sv = str(v).strip() - return sv == '' or sv == '_' or sv.lower() == 'null' - - -def emit_dl_value(lines, type_str, val, indent, value_list_allowed=False): - if is_dl_empty_value(val): - # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil (даже при известном типе). - if value_list_allowed: - return - lines.append(f'{indent}') - return - if isinstance(val, bool): - val_str = 'true' if val else 'false' - else: - val_str = str(val) - t = type_str or '' - if re.match(r'^(date|dateTime|time)', t): - lines.append(f'{indent}{esc_xml(val_str)}') - elif t == 'boolean': - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^decimal', t): - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^string', t): - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t): - lines.append(f'{indent}{esc_xml(val_str)}') - else: - if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): - lines.append(f'{indent}{esc_xml(val_str)}') - elif val_str in ('true', 'false'): - lines.append(f'{indent}{esc_xml(val_str)}') - elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str): - lines.append(f'{indent}{esc_xml(val_str)}') - else: - lines.append(f'{indent}{esc_xml(val_str)}') - - -def emit_dl_value_type(lines, type_str, indent): - if not type_str: - return - lines.append(f'{indent}') - for part in re.split(r'\s*[|+]\s*', str(type_str)): - emit_single_type(lines, part.strip(), f'{indent}\t') - lines.append(f'{indent}') - - -def emit_dl_available_value(lines, av, type_str, indent): - lines.append(f'{indent}') - av_val = av.get('value') if isinstance(av, dict) else None - emit_dl_value(lines, type_str, av_val, f'{indent}\t', False) - pres = (av.get('presentation') or av.get('title')) if isinstance(av, dict) else None - if pres: - emit_dl_mltext(lines, f'{indent}\t', 'dcssch:presentation', pres) - lines.append(f'{indent}') - - -def emit_dl_input_parameters(lines, ip, indent): - if ip is None: - return - items = ip if isinstance(ip, list) else [ip] - if len(items) == 0: - return - lines.append(f'{indent}') - for item in items: - lines.append(f'{indent}\t') - if 'use' in item and item.get('use') is not None and not item.get('use'): - lines.append(f'{indent}\t\tfalse') - lines.append(f'{indent}\t\t{esc_xml(str(item.get("parameter", "")))}') - if 'choiceParameters' in item: - cp_items = item.get('choiceParameters') or [] - if len(cp_items) == 0: - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - for cp in cp_items: - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(cp.get("name", "")))}') - for v in (cp.get('values') or []): - if isinstance(v, bool): - lines.append(f'{indent}\t\t\t\t{"true" if v else "false"}') - elif isinstance(v, (int, float)): - lines.append(f'{indent}\t\t\t\t{v}') - else: - lines.append(f'{indent}\t\t\t\t{esc_xml(str(v))}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - elif 'choiceParameterLinks' in item: - cpl_items = item.get('choiceParameterLinks') or [] - if len(cpl_items) == 0: - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - for cpl in cpl_items: - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("name", "")))}') - lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("value", "")))}') - mode = str(cpl.get('mode') or 'Auto') - lines.append(f'{indent}\t\t\t\t{mode}') - lines.append(f'{indent}\t\t\t') - lines.append(f'{indent}\t\t') - elif 'value' in item: - val = item.get('value') - if isinstance(val, bool): - lines.append(f'{indent}\t\t{"true" if val else "false"}') - elif isinstance(val, (int, float)): - lines.append(f'{indent}\t\t{val}') - elif isinstance(val, dict): - emit_dl_mltext(lines, f'{indent}\t\t', 'dcscor:value', val) - else: - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - lines.append(f'{indent}\t') - lines.append(f'{indent}') - - -# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd ── -def _test_empty_value(v): - if v is None: - return True - s = str(v).strip() - return s == '' or s == '_' or s.lower() == 'null' - - -def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False): - if value_list_allowed: - return - t = type_str or '' - t_bare = t[3:] if t.startswith('xs:') else t - pf = tag_prefix - if t == '': - lines.append(f'{indent}<{pf}value xsi:nil="true"/>') - elif t == 'StandardPeriod': - lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">') - lines.append(f'{indent}\tCustom') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}\t0001-01-01T00:00:00') - lines.append(f'{indent}') - elif re.match(r'^string', t_bare): - lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>') - elif re.match(r'^(date|time)', t_bare): - lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00') - elif re.match(r'^decimal', t_bare): - lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0') - elif t_bare == 'boolean': - lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false') - else: - lines.append(f'{indent}<{pf}value xsi:nil="true"/>') - - -_DP_PERIOD_VARIANTS = {"Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear"} - - -def parse_data_param_shorthand(s): - result = {'parameter': '', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None} - if '@user' in s: - result['userSettingID'] = 'auto'; s = re.sub(r'\s*@user', '', s) - if '@off' in s: - result['use'] = False; s = re.sub(r'\s*@off', '', s) - if '@quickAccess' in s: - result['viewMode'] = 'QuickAccess'; s = re.sub(r'\s*@quickAccess', '', s) - if '@normal' in s: - result['viewMode'] = 'Normal'; s = re.sub(r'\s*@normal', '', s) - s = s.strip() - m = re.match(r'^([^=]+)=\s*(.+)$', s) - if m: - result['parameter'] = m.group(1).strip() - val_str = m.group(2).strip() - if val_str in _DP_PERIOD_VARIANTS: - result['value'] = {'variant': val_str} - elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): - result['value'] = val_str - elif val_str in ('true', 'false'): - result['value'] = (val_str == 'true') - else: - result['value'] = val_str - else: - result['parameter'] = s - return result - - -def emit_data_parameters(lines, items, indent, block_view_mode=None): - if not items or len(items) == 0: - return - lines.append(f'{indent}') - for dp in items: - if isinstance(dp, str): - parsed = parse_data_param_shorthand(dp) - dp = {'parameter': parsed['parameter']} - if parsed['value'] is not None: - dp['value'] = parsed['value'] - if parsed['use'] is False: - dp['use'] = False - if parsed['userSettingID']: - dp['userSettingID'] = parsed['userSettingID'] - if parsed['viewMode']: - dp['viewMode'] = parsed['viewMode'] - lines.append(f'{indent}\t') - if dp.get('use') is False: - lines.append(f'{indent}\t\tfalse') - lines.append(f'{indent}\t\t{esc_xml(str(dp.get("parameter", "")))}') - vtype = str(dp.get('valueType') or '') - val = dp.get('value') - if dp.get('nilValue') is True: - lines.append(f'{indent}\t\t') - elif _test_empty_value(val) and vtype: - emit_empty_value(lines, vtype, f'{indent}\t\t', tag_prefix='dcscor:', value_list_allowed=False) - elif _test_empty_value(val): - pass # нет значения → не эмитим value-узел (form дин-список: use=false плейсхолдер) - elif val is not None: - if isinstance(val, dict) and val.get('variant'): - variant = str(val.get('variant')) - has_date = 'date' in val - has_sd = 'startDate' in val - is_sbd = has_date or (not has_sd and variant.startswith('BeginningOf')) - if is_sbd: - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t{esc_xml(variant)}') - if variant == 'Custom': - d = str(val.get('date') or '0001-01-01T00:00:00') - lines.append(f'{indent}\t\t\t{esc_xml(d)}') - lines.append(f'{indent}\t\t') - else: - lines.append(f'{indent}\t\t') - lines.append(f'{indent}\t\t\t{esc_xml(variant)}') - if variant == 'Custom': - sd = str(val.get('startDate') or '0001-01-01T00:00:00') - ed = str(val.get('endDate') or '0001-01-01T00:00:00') - lines.append(f'{indent}\t\t\t{esc_xml(sd)}') - lines.append(f'{indent}\t\t\t{esc_xml(ed)}') - lines.append(f'{indent}\t\t') - elif re.match(r'^[a-zA-Z]+:', vtype): - v_str = str(val).lower() if isinstance(val, bool) else str(val) - lines.append(f'{indent}\t\t{esc_xml(v_str)}') - elif vtype == 'boolean' or isinstance(val, bool): - lines.append(f'{indent}\t\t{esc_xml(str(val).lower())}') - elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - elif re.match(r'^decimal', vtype): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - elif re.match(r'^string', vtype): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)): - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - else: - lines.append(f'{indent}\t\t{esc_xml(str(val))}') - if dp.get('viewMode'): - lines.append(f'{indent}\t\t{esc_xml(str(dp["viewMode"]))}') - if dp.get('userSettingID'): - uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID']) - lines.append(f'{indent}\t\t{esc_xml(uid)}') - if dp.get('userSettingPresentation'): - emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation']) - lines.append(f'{indent}\t') - if block_view_mode is not None: - lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') - lines.append(f'{indent}') - - -def emit_dl_parameter(lines, p, parsed, indent): - is_obj = not isinstance(p, str) - lines.append(f'{indent}') - ci = f'{indent}\t' - lines.append(f'{ci}{esc_xml(parsed["name"])}') - # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. - title = None - if parsed.get('title'): - title = parsed['title'] - elif is_obj and p.get('title'): - title = p['title'] - elif is_obj and p.get('presentation'): - title = p['presentation'] - if title is None or (isinstance(title, str) and title == ''): - title = title_from_name(parsed['name']) - emit_dl_mltext(lines, ci, 'dcssch:title', title) - # valueType - if parsed.get('type'): - emit_dl_value_type(lines, parsed['type'], ci) - # value (дефолт nil; при valueListAllowed пустое — опускаем) - vla = bool(parsed.get('valueListAllowed')) - pv = parsed.get('value') - if isinstance(pv, list): - for v in pv: - emit_dl_value(lines, parsed.get('type', ''), v, ci, False) - elif vla and is_dl_empty_value(pv) and parsed.get('value_explicit'): - # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа пишет nil - lines.append(f'{ci}') - else: - emit_dl_value(lines, parsed.get('type', ''), pv, ci, vla) - # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. - ur = True - if is_obj and 'useRestriction' in p: - ur = bool(p['useRestriction']) - lines.append(f'{ci}{"true" if ur else "false"}') - # expression - expr = str(p['expression']) if (is_obj and p.get('expression')) else None - if expr: - lines.append(f'{ci}{esc_xml(expr)}') - # availableValues - if is_obj and p.get('availableValues'): - for av in p['availableValues']: - emit_dl_available_value(lines, av, parsed.get('type', ''), ci) - # valueListAllowed - if vla: - lines.append(f'{ci}true') - # availableAsField=false (hidden или явный) - aaf = None - if parsed.get('hidden') is True: - aaf = False - if is_obj and 'availableAsField' in p: - aaf = bool(p['availableAsField']) - if aaf is False: - lines.append(f'{ci}false') - # inputParameters - if is_obj and p.get('inputParameters'): - emit_dl_input_parameters(lines, p['inputParameters'], ci) - # denyIncompleteValues - if is_obj and p.get('denyIncompleteValues') is True: - lines.append(f'{ci}true') - # use - if is_obj and p.get('use'): - lines.append(f'{ci}{esc_xml(str(p["use"]))}') - lines.append(f'{indent}') - - -def emit_dl_parameters(lines, params, indent): - if not params: - return - for p in params: - if isinstance(p, str): - parsed = parse_dl_param_shorthand(p) - else: - resolved_type = '' - if p.get('type'): - if isinstance(p['type'], list): - resolved_type = ' | '.join(resolve_type_str(str(x)) for x in p['type']) - else: - resolved_type = resolve_type_str(str(p['type'])) - elif p.get('valueType'): - resolved_type = resolve_type_str(str(p['valueType'])) - parsed = {'name': str(p.get('name', '')), 'type': resolved_type, - 'value': p.get('value') if 'value' in p else None, - 'value_explicit': ('value' in p), 'title': None} - if p.get('valueListAllowed') is True: - parsed['valueListAllowed'] = True - if p.get('hidden') is True: - parsed['hidden'] = True - emit_dl_parameter(lines, p, parsed, indent) - - -def emit_attributes(lines, attrs, indent, conditional_appearance=None): - has_ca = bool(conditional_appearance) and len(conditional_appearance) > 0 - # Платформа ВСЕГДА эмитит (100% корпуса; 162 формы — пустой ). - if (not attrs or len(attrs) == 0) and not has_ca: - lines.append(f'{indent}') - return - if not attrs or len(attrs) == 0: - # Нет реквизитов, но есть условное оформление (последний child ) - lines.append(f'{indent}') - emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') - lines.append(f'{indent}') - return - - lines.append(f'{indent}') - seen_attrs = set() - for attr in attrs: - attr_id = new_id() - attr_name = str(attr['name']) - _ensure_unique(attr_name, seen_attrs, 'attribute') - - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - # Title атрибута (зеркало emit_title): нет ключа → авто-вывод из имени (кроме main); - # title "" → подавить; непустой → эмитить как есть. - if 'title' in attr: - if attr.get('title'): - emit_mltext(lines, inner, 'Title', attr['title']) - elif attr.get('main') is not True: - emit_mltext(lines, inner, 'Title', title_from_name(attr_name)) - - # Type - if attr.get('type'): - emit_type(lines, str(attr['type']), inner) - else: - lines.append(f'{inner}') - # valueType: ОписаниеТипов значений ValueList → - # (та же грамматика типа, включая составной "A | B"). Forgiving-синонимы. - # Три состояния: нет ключа → нет Settings; "" → пустой ; тип → с типом. - vt_spec = None - has_vt = False - for k in ('valueType', 'typeDescription', 'описаниеТипов', 'типЗначений'): - if k in attr: - vt_spec = attr[k] - has_vt = True - break - if has_vt: - emit_type(lines, '' if vt_spec is None else str(vt_spec), inner, tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"') - # Planner design-time (встроенный конфиг планировщика). - if attr.get('planner') is not None: - emit_planner_settings(lines, attr['planner'], inner) - # Chart/GanttChart design-time (тип выводится из типа реквизита). - if attr.get('chart') is not None: - ctype = 'd4p1:GanttChart' if 'GanttChart' in str(attr.get('type', '')) else 'd4p1:Chart' - emit_chart_settings(lines, attr['chart'], inner, ctype) - - if attr.get('main') is True: - lines.append(f'{inner}true') - # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) - if attr.get('view') is not None: - emit_xr_flag(lines, 'View', attr.get('view'), inner) - if attr.get('edit') is not None: - emit_xr_flag(lines, 'Edit', attr.get('edit'), inner) - main_saved = False - if attr.get('main') is True and attr.get('type'): - t = str(attr['type']) - main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) - # Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто. - emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved - if emit_saved: - lines.append(f'{inner}true') - # Save: сохранение значения реквизита в пользовательских настройках. true → имя; - # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). - # Нет ключа или false → не эмитим. - if 'save' in attr and attr['save'] is not None: - save_fields = [] - sv = attr['save'] - if isinstance(sv, bool): - if sv: - save_fields.append(attr_name) - else: - for e in (sv if isinstance(sv, (list, tuple)) else [sv]): - fld = str(e) - if not fld: - continue - if fld != attr_name and '.' not in fld and not re.match(r'^\d+/\d+:', fld): - fld = f'{attr_name}.{fld}' - if fld not in save_fields: - save_fields.append(fld) - if save_fields: - lines.append(f'{inner}') - for f in save_fields: - lines.append(f'{inner}\t{esc_xml(f)}') - lines.append(f'{inner}') - # Проверка заполнения → (реальный тег; в схеме нет). - # bool true → ShowError; строка → verbatim. Синоним fillChecking. - fc_raw = attr['fillCheck'] if 'fillCheck' in attr else attr.get('fillChecking') - if fc_raw: - fcv = 'ShowError' if isinstance(fc_raw, bool) else str(fc_raw) - lines.append(f'{inner}{fcv}') - - # UseAlways: поля, всегда читаемые. Две формы DSL сливаются: - # attr.useAlways[] (короткие имена) + columns с useAlways:true → ИмяРеквизита.Поле. - ua_fields = [] - for e in (attr.get('useAlways') or []): - fld = str(e) - # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" - # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. - # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). - if fld.startswith('~'): - bare = fld[1:] - if not re.match(r'^' + re.escape(attr_name) + r'\.', bare): - bare = f'{attr_name}.{bare}' - fld = f'~{bare}' - elif not re.match(r'^' + re.escape(attr_name) + r'\.', fld): - fld = f'{attr_name}.{fld}' - if fld not in ua_fields: - ua_fields.append(fld) - for col in (attr.get('columns') or []): - if col.get('useAlways') is True: - fld = f'{attr_name}.{col["name"]}' - if fld not in ua_fields: - ua_fields.append(fld) - if ua_fields: - lines.append(f'{inner}') - for f in ua_fields: - lines.append(f'{inner}\t{f}') - lines.append(f'{inner}') - - emit_functional_options(lines, attr.get('functionalOptions'), inner) - - # Columns: прямые + (доп. колонки табличных частей объекта). - # Прямые сначала, затем AdditionalColumns-группы. Для дин-списка (settings) прямые НЕ эмитим. - has_direct_cols = bool(attr.get('columns')) and len(attr['columns']) > 0 and not attr.get('settings') - has_add_cols = bool(attr.get('additionalColumns')) and len(attr['additionalColumns']) > 0 - if has_direct_cols or has_add_cols: - lines.append(f'{inner}') - if has_direct_cols: - seen_cols = set() # колонки уникальны в пределах своего реквизита - for col in attr['columns']: - _ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'") - emit_attr_column(lines, col, f'{inner}\t') - if has_add_cols: - for ac in attr['additionalColumns']: - ac_cols = ac.get('columns') or [] - if not ac_cols: - # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) - lines.append(f'{inner}\t') - continue - lines.append(f'{inner}\t') - seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns - for col in ac_cols: - _ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'") - emit_attr_column(lines, col, f'{inner}\t\t') - lines.append(f'{inner}\t') - lines.append(f'{inner}') - - # Settings (динамический список) - if attr.get('settings'): - s = attr['settings'] - lines.append(f'{inner}') - si = f'{inner}\t' - # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings - # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). - if s.get('autoFillAvailableFields') is not None: - lines.append(f'{si}{"true" if s["autoFillAvailableFields"] else "false"}') - # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings - has_query = bool(s.get('query') and str(s['query']).strip()) - mq = 'true' if (has_query or s.get('manualQuery')) else 'false' - lines.append(f'{si}{mq}') - # DynamicDataRead: дефолт true; false только при явном отключении - ddr = 'false' if s.get('dynamicDataRead') is False else 'true' - lines.append(f'{si}{ddr}') - if has_query: - qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR) - lines.append(f'{si}{esc_xml(qtext)}') - # Явные поля набора (редко): override title/dataPath - if s.get('fields'): - for fld in s['fields']: - lines.append(f'{si}') - dp = fld.get('dataPath') or fld.get('field') - lines.append(f'{si}\t{esc_xml(str(dp))}') - lines.append(f'{si}\t{esc_xml(str(fld.get("field", "")))}') - if fld.get('title'): - lines.append(f'{si}\t') - emit_ml_items(lines, f'{si}\t\t', fld['title']) - lines.append(f'{si}\t') - lines.append(f'{si}') - # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. - emit_dl_parameters(lines, s.get('parameters'), si) - if s.get('mainTable'): - lines.append(f'{si}{normalize_meta_type_ref(str(s["mainTable"]))}') - # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). - if s.get('autoSaveUserSettings') is not None: - lines.append(f'{si}{"true" if s["autoSaveUserSettings"] else "false"}') - # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. - # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. - lsi = f'{si}\t' - lines.append(f'{si}') - ls_shape = s.get('listSettings') - if ls_shape is not None: - # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. - for tag, meta in ls_shape.items(): - meta = str(meta) - bvm = 'Normal' if 'v' in meta else None - if tag == 'filter': - bus = CANON_FILTER_ID if 'u' in meta else None - emit_filter(lines, s.get('filter'), lsi, block_view_mode=bvm, block_user_setting_id=bus) - elif tag == 'order': - bus = CANON_ORDER_ID if 'u' in meta else None - emit_order(lines, s.get('order'), lsi, block_view_mode=bvm, block_user_setting_id=bus) - elif tag == 'conditionalAppearance': - bus = CANON_CA_ID if 'u' in meta else None - emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode=bvm, block_user_setting_id=bus) - elif tag == 'itemsViewMode': - lines.append(f'{lsi}Normal') - elif tag == 'itemsUserSettingID': - lines.append(f'{lsi}{CANON_ITEMS_ID}') - else: - # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. - emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) - # dataParameters — после filter, до order (XSD-порядок ListSettings) - if 'dataParameters' in s: - emit_data_parameters(lines, s.get('dataParameters'), lsi) - emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID) - emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) - lines.append(f'{lsi}Normal') - lines.append(f'{lsi}{CANON_ITEMS_ID}') - lines.append(f'{si}') - lines.append(f'{inner}') - - lines.append(f'{indent}\t') - # Условное оформление формы — последний child (та же DCS-грамматика, что settings CA) - emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') - lines.append(f'{indent}') - - -# --- Parameter emitter --- - -def emit_parameters(lines, params, indent): - if not params or len(params) == 0: - return - - lines.append(f'{indent}') - seen_params = set() - for param in params: - _ensure_unique(str(param['name']), seen_params, 'parameter') - 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}') - seen_cmds = set() - for cmd in cmds: - cmd_id = new_id() - _ensure_unique(str(cmd['name']), seen_cmds, 'command') - lines.append(f'{indent}\t') - inner = f'{indent}\t\t' - - # Заголовок команды (зеркало emit_title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс - # (в оригинале нет — не додумывать); ключ отсутствует → авто-вывод из имени. - if 'title' in cmd: - if cmd['title']: - emit_mltext(lines, inner, 'Title', cmd['title']) - else: - cmd_title = title_from_name(str(cmd['name'])) - if cmd_title: - emit_mltext(lines, inner, 'Title', cmd_title) - - if cmd.get('tooltip'): - emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) - - # Доступность команды по ролям (после ToolTip, до Action) - if cmd.get('use') is not None: - emit_xr_flag(lines, 'Use', cmd.get('use'), inner) - - if cmd.get('action'): - lines.append(f'{inner}<Action>{cmd["action"]}</Action>') - - if cmd.get('modifiesSavedData') is True: - lines.append(f'{inner}<ModifiesSavedData>true</ModifiesSavedData>') - - emit_functional_options(lines, cmd.get('functionalOptions'), inner) - - if cmd.get('currentRowUse'): - lines.append(f'{inner}<CurrentRowUse>{cmd["currentRowUse"]}</CurrentRowUse>') - - # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). - # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) - _cmd_norm = {k.replace(' ', '').lower(): v for k, v in cmd.items()} - cmd_table = (_cmd_norm.get('table') or _cmd_norm.get('associatedtableelementid') - or _cmd_norm.get('используемаятаблица')) - if cmd_table: - lines.append(f'{inner}<AssociatedTableElementId xsi:type="xs:string">{esc_xml(str(cmd_table))}</AssociatedTableElementId>') - - if cmd.get('shortcut'): - lines.append(f'{inner}<Shortcut>{cmd["shortcut"]}</Shortcut>') - - emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner) - - if cmd.get('representation'): - lines.append(f'{inner}<Representation>{cmd["representation"]}</Representation>') - - lines.append(f'{indent}\t</Command>') - lines.append(f'{indent}</Commands>') - - -# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. -# Элемент: строка (голый command, Type=Auto) или dict. Порядок тегов: -# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). -def _resolve_command_group_key(key, panel_tag): - """Ключ-группа древовидной формы → CommandGroup (зависит от панели); иначе verbatim.""" - k = re.sub(r'\s', '', str(key)).lower() - if panel_tag == 'NavigationPanel': - m = {'important': 'FormNavigationPanelImportant', 'важное': 'FormNavigationPanelImportant', - 'goto': 'FormNavigationPanelGoTo', 'перейти': 'FormNavigationPanelGoTo', - 'seealso': 'FormNavigationPanelSeeAlso', 'смтакже': 'FormNavigationPanelSeeAlso'} - else: - m = {'important': 'FormCommandBarImportant', 'важное': 'FormCommandBarImportant', - 'createbasedon': 'FormCommandBarCreateBasedOn', 'создатьнаосновании': 'FormCommandBarCreateBasedOn'} - return m.get(k, key) - - -def emit_command_interface(lines, ci, indent): - if not ci: - return - inner = f'{indent}\t' - panels = [ - ('CommandBar', ('commandBar', 'команднаяПанель', 'КоманднаяПанель')), - ('NavigationPanel', ('navigationPanel', 'панельНавигации', 'ПанельНавигации')), - ] - present = [] - for tag, syns in panels: - items = None - for syn in syns: - if isinstance(ci, dict) and syn in ci: - items = ci[syn] - break - if items is not None: - present.append((tag, items)) - if not present: - return - lines.append(f'{indent}<CommandInterface>') - for tag, items in present: - lines.append(f'{inner}<{tag}>') - # Нормализация: плоский список пар (элемент, group-из-дерева). dict → древовидная форма. - flat = [] - if isinstance(items, dict): - for gkey, gitems in items.items(): - grp_tree = _resolve_command_group_key(gkey, tag) - for it in gitems: - flat.append((it, grp_tree)) - else: - for it in items: - flat.append((it, None)) - for item, tree_group in flat: - if isinstance(item, str): - cmd, typ, attr, grp, idx, dv, vis = item, 'Auto', None, None, None, None, None - else: - cmd = get_el_prop(item, ('command', 'команда')) - typ = get_el_prop(item, ('type', 'тип')) or 'Auto' - attr = get_el_prop(item, ('attribute', 'реквизит')) - grp = get_el_prop(item, ('group', 'группа', 'группаКоманд')) - idx = get_el_prop(item, ('index', 'индекс')) - dv = get_el_prop(item, ('defaultVisible', 'видимость', 'видимостьПоУмолчанию')) - vis = get_el_prop(item, ('visible', 'видимостьПоРолям', 'настройкаВидимости')) - # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк - if tree_group: - grp = tree_group - lines.append(f'{inner}\t<Item>') - lines.append(f'{inner}\t\t<Command>{esc_xml(str(cmd))}</Command>') - lines.append(f'{inner}\t\t<Type>{typ}</Type>') - if attr: - lines.append(f'{inner}\t\t<Attribute>{esc_xml(str(attr))}</Attribute>') - if grp: - lines.append(f'{inner}\t\t<CommandGroup>{esc_xml(str(grp))}</CommandGroup>') - if idx is not None: - lines.append(f'{inner}\t\t<Index>{idx}</Index>') - if dv is not None: - lines.append(f'{inner}\t\t<DefaultVisible>{"true" if dv else "false"}</DefaultVisible>') - if vis is not None: - emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t') - lines.append(f'{inner}\t</Item>') - lines.append(f'{inner}</{tag}>') - lines.append(f'{indent}</CommandInterface>') - - -# --- 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:] - - # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) - if isinstance(p_value, str) and p_value == '': - continue - # 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}</{xml_name}>') - - - -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'<MetaDataObject[^>]+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 _normalize_elements(defn): - """Convert dict-style elements from --from-object generators to list-style expected by compiler. - Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} - Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] - Also handles nested 'elements' in groups and 'columns' in tables recursively. - """ - def convert_elements(els): - if isinstance(els, list): - # Already list format — but may have nested dicts inside groups - result = [] - for el in els: - if isinstance(el, dict): - el = dict(el) # copy - if 'elements' in el and isinstance(el['elements'], dict): - el['elements'] = convert_elements(el['elements']) - if 'columns' in el and isinstance(el['columns'], dict): - el['columns'] = convert_columns(el['columns']) - result.append(el) - return result - if isinstance(els, dict): - result = [] - for name, props in els.items(): - if not isinstance(props, dict): - continue - new_el = {} - el_type = props.get('element', 'input') - # Map element type to the key name used in JSON DSL - type_map = { - 'input': 'input', 'check': 'check', 'labelField': 'labelField', - 'table': 'table', 'group': 'group', 'pages': 'pages', - 'page': 'page', 'label': 'label', 'button': 'button', - 'checkBox': 'check', 'radioButton': 'radioButton', - 'pictureField': 'pictureField', - } - mapped_type = type_map.get(el_type, el_type) - new_el[mapped_type] = name - for k, v in props.items(): - if k == 'element': - continue - if k == 'elements' and isinstance(v, dict): - new_el['elements'] = convert_elements(v) - elif k == 'columns' and isinstance(v, dict): - new_el['columns'] = convert_columns(v) - elif k == 'groupType': - # groupType → group property in DSL - new_el['group'] = v - elif k == 'showTitle': - new_el['showTitle'] = v - elif k == 'representation': - new_el['representation'] = v - elif k == 'autoCommandBar': - new_el['autoCommandBar'] = v - elif k == 'commandBarLocation': - new_el['commandBarLocation'] = v - else: - new_el[k] = v - result.append(new_el) - return result - return els - - def convert_columns(cols): - if isinstance(cols, list): - return cols - if isinstance(cols, dict): - result = [] - for name, props in cols.items(): - if not isinstance(props, dict): - continue - new_col = {} - el_type = props.get('element', 'input') - type_map = { - 'input': 'input', 'check': 'check', 'labelField': 'labelField', - 'checkBox': 'check', - } - mapped_type = type_map.get(el_type, el_type) - new_col[mapped_type] = name - for k, v in props.items(): - if k == 'element': - continue - new_col[k] = v - result.append(new_col) - return result - return cols - - if 'elements' in defn: - defn['elements'] = convert_elements(defn['elements']) - return defn - - -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', # ФормаГруппы - '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи - '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета - '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла - } - - # 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'], - 'InformationRegister': ['Record', 'List'], - 'AccumulationRegister': ['List'], - 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], - 'ExchangePlan': ['Item', 'List', 'Choice'], - 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], - } - if meta['Type'] not in supported: - print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", 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) - - dsl_dispatch = { - 'Document': generate_document_dsl, - 'Catalog': generate_catalog_dsl, - 'InformationRegister': generate_information_register_dsl, - 'AccumulationRegister': generate_accumulation_register_dsl, - 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, - 'ExchangePlan': generate_exchange_plan_dsl, - 'ChartOfAccounts': generate_chart_of_accounts_dsl, - } - dsl = dsl_dispatch[meta['Type']](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 - # Convert dict-style elements (from generators) to list-style (expected by compiler) - defn = _normalize_elements(defn) - 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) - global QUERY_BASE_DIR - QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path)) - - # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- - def _normalize_synonyms(el): - if not isinstance(el, dict): - return - # Companion-панели (объект/массив-значение) → commandBar/contextMenu - normalize_panel_synonyms(el) - # Тип-синонимы: commandBar/autoCommandBar → элемент-тип ТОЛЬКО при строковом значении - synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar', 'extTooltip': 'extendedTooltip'} - for src, dst in synonyms.items(): - if src in el and dst not in el: - if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): - continue - el[dst] = el.pop(src) - # Рекурсия в детей панелей (commandBar/contextMenu) - for pk in ('commandBar', 'contextMenu'): - pv = el.get(pk) - kids = pv if isinstance(pv, list) else (pv.get('children') if isinstance(pv, dict) else None) - if isinstance(kids, list): - for child in kids: - _normalize_synonyms(child) - if isinstance(el.get('children'), list): - for child in el['children']: - _normalize_synonyms(child) - if isinstance(el.get('columns'), list): - for child in el['columns']: - _normalize_synonyms(child) - - def _has_cmd_bar_recursive(el): - if not isinstance(el, dict): - return False - if el.get('cmdBar') is not None: - return True - if isinstance(el.get('children'), list): - for child in el['children']: - if _has_cmd_bar_recursive(child): - return True - if isinstance(el.get('columns'), list): - for child in el['columns']: - if _has_cmd_bar_recursive(child): - return True - return False - - def _apply_dlist_table_heuristic(el, list_name, has_main_table): - if not isinstance(el, dict): - return - if el.get('table') is not None and str(el.get('path', '')) == list_name: - # Маркер дин-список-таблицы → emit_table эмитит блок свойств - el['_dynList'] = True - if 'tableAutofill' not in el: - el['tableAutofill'] = False - if 'commandBarLocation' not in el: - el['commandBarLocation'] = 'None' - # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. - # Декомпилятор опускает при rpdp == smart-default; реальное отсутствие → ""-маркер (не - # перезатирается). Гейт has_main_table снят: дин-список без mainTable тоже несёт RowPictureDataPath. - if 'rowPictureDataPath' not in el: - el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' - if isinstance(el.get('children'), list): - for child in el['children']: - _apply_dlist_table_heuristic(child, list_name, has_main_table) - - def _is_object_like_type(t): - if not t: - return False - if t == 'DynamicList' or t == 'ConstantsSet': - return True - object_suffixes = ( - 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', - 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', - 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', - 'ChartOfCalculationTypesObject', 'ExchangePlanObject', - ) - record_set_prefixes = ( - 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', - 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', - 'InformationRegisterRecordManager', - ) - for s in object_suffixes: - if t.startswith(s + '.'): - return True - for s in record_set_prefixes: - if t.startswith(s + '.'): - return True - return False - - # 1b.1: Normalize synonyms recursively - if isinstance(defn.get('elements'), list): - for el in defn['elements']: - _normalize_synonyms(el) - - # 1b.2: Extract autoCmdBar element from defn['elements'] - main_acb_def = None - if isinstance(defn.get('elements'), list): - auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] - if len(auto_bars) > 1: - print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) - sys.exit(1) - if len(auto_bars) == 1: - main_acb_def = auto_bars[0] - defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] - - # 1b.3: Infer main attribute - if isinstance(defn.get('attributes'), list): - has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) - if not has_explicit_main: - candidates = [] - for a in defn['attributes']: - if not isinstance(a, dict): - continue - if 'main' in a and a.get('main') is False: - continue - if _is_object_like_type(str(a.get('type', ''))): - candidates.append(a) - if len(candidates) == 1: - candidates[0]['main'] = True - print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") - elif len(candidates) > 1: - names = ', '.join(c.get('name', '') for c in candidates) - print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") - - # 1b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) - if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): - for attr in defn['attributes']: - if not isinstance(attr, dict) or str(attr.get('type', '')) != 'DynamicList': - continue - settings = attr.get('settings') or {} - has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) - for el in defn['elements']: - _apply_dlist_table_heuristic(el, attr.get('name', ''), has_mt) - - # 1b.5: Compute main AutoCommandBar Autofill (B3) - def _compute_main_acb_autofill(): - if main_acb_def is not None: - if 'autofill' in main_acb_def: - return bool(main_acb_def.get('autofill')) - return True - if isinstance(defn.get('elements'), list): - for el in defn['elements']: - if _has_cmd_bar_recursive(el): - return False - return True - - # --- 2. Main compilation --- - _next_id = 0 - _seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе) - lines = [] - - lines.append('<?xml version="1.0" encoding="UTF-8"?>') - lines.append(f'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">') - - # 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', form_title) - - # Properties (skip 'title' — handled above) - # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; - # otherwise platform appends synonym → "Title: Synonym" double-titles). - props_src = defn.get('properties') or {} - props_clone = OrderedDict() - if form_title and 'autoTitle' not in props_src: - props_clone['autoTitle'] = False - for k, v in props_src.items(): - if k != 'title': - props_clone[k] = v - emit_properties(lines, props_clone, '\t') - - # CommandSet (excluded commands) - if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: - lines.append('\t<CommandSet>') - for cmd in defn['excludedCommands']: - lines.append(f'\t\t<ExcludedCommand>{cmd}</ExcludedCommand>') - lines.append('\t</CommandSet>') - - # MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок - # (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). - if defn.get('mobileCommandBarContent') and len(defn['mobileCommandBarContent']) > 0: - lines.append('\t<MobileDeviceCommandBarContent>') - for nm in defn['mobileCommandBarContent']: - lines.append('\t\t<xr:Item>') - lines.append('\t\t\t<xr:Presentation/>') - lines.append('\t\t\t<xr:CheckState>0</xr:CheckState>') - lines.append(f'\t\t\t<xr:Value xsi:type="xs:string">{esc_xml(str(nm))}</xr:Value>') - lines.append('\t\t</xr:Item>') - lines.append('\t</MobileDeviceCommandBarContent>') - - # AutoCommandBar (always present, id=-1) - acb_autofill = _compute_main_acb_autofill() - acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' - acb_halign = None - if main_acb_def is not None: - v = main_acb_def.get('autoCmdBar') - if v: - acb_name = str(v) - if main_acb_def.get('name'): - acb_name = str(main_acb_def['name']) - if main_acb_def.get('horizontalAlign'): - acb_halign = str(main_acb_def['horizontalAlign']) - has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) - has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children - if has_inner: - lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">') - if acb_halign: - lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>') - if not acb_autofill: - lines.append('\t\t<Autofill>false</Autofill>') - if has_acb_children: - lines.append('\t\t<ChildItems>') - for child in main_acb_def['children']: - emit_element(lines, child, '\t\t\t', in_cmd_bar=True) - lines.append('\t\t</ChildItems>') - lines.append('\t</AutoCommandBar>') - else: - lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>') - - # 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<Events>') - for evt_name, evt_handler in defn['events'].items(): - lines.append(f'\t\t<Event name="{evt_name}">{evt_handler}</Event>') - lines.append('\t</Events>') - - # ChildItems (elements) - if defn.get('elements') and len(defn['elements']) > 0: - lines.append('\t<ChildItems>') - for el in defn['elements']: - emit_element(lines, el, '\t\t') - lines.append('\t</ChildItems>') - - # Attributes - emit_attributes(lines, defn.get('attributes'), '\t', conditional_appearance=defn.get('conditionalAppearance')) - - # Parameters - emit_parameters(lines, defn.get('parameters'), '\t') - - # Commands - emit_commands(lines, defn.get('commands'), '\t') - - # CommandInterface (командный интерфейс формы — последний дочерний Form) - emit_command_interface(lines, defn.get('commandInterface'), '\t') - - # Close - lines.append('</Form>') - - # --- 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>{form_name}</Form>' not in raw_text: - # Insert before </ChildObjects> - if '</ChildObjects>' in raw_text: - insert_line = f'\t\t\t<Form>{form_name}</Form>\n' - raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1) - elif '<ChildObjects/>' in raw_text: - replacement = f'<ChildObjects>\n\t\t\t<Form>{form_name}</Form>\n\t\t</ChildObjects>' - raw_text = raw_text.replace('<ChildObjects/>', replacement, 1) - - write_utf8_bom(object_xml_path, raw_text) - print(f" Registered: <Form>{form_name}</Form> 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.138 — 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_fields(parent_node, tag_name='Attribute'): + """Extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag).""" + result = [] + if parent_node is None: + return result + for field_node in _et_findall(parent_node, f'md:{tag_name}'): + fp = _et_find(field_node, 'md:Properties') + f_name = _et_text(fp, 'md:Name') + f_syn_node = _et_find(fp, "md:Synonym/v8:item[v8:lang='ru']/v8:content") + f_syn = f_syn_node.text if f_syn_node is not None and f_syn_node.text else f_name + f_type_node = _et_find(fp, 'md:Type') + f_type = extract_type(f_type_node) + result.append({ + 'Name': f_name, + 'Synonym': f_syn, + 'Type': f_type, + 'IsRef': is_ref_type(f_type), + }) + return result + + # Attributes + attributes = extract_fields(child_objs, 'Attribute') + + # 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_fields(ts_co, 'Attribute') + 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 + elif obj_type == 'InformationRegister': + meta['Dimensions'] = extract_fields(child_objs, 'Dimension') + meta['Resources'] = extract_fields(child_objs, 'Resource') + prd_node = _et_find(props_node, 'md:InformationRegisterPeriodicity') + meta['Periodicity'] = prd_node.text if prd_node is not None and prd_node.text else 'Nonperiodical' + wm_node = _et_find(props_node, 'md:WriteMode') + meta['WriteMode'] = wm_node.text if wm_node is not None and wm_node.text else 'Independent' + elif obj_type == 'AccumulationRegister': + meta['Dimensions'] = extract_fields(child_objs, 'Dimension') + meta['Resources'] = extract_fields(child_objs, 'Resource') + rt_node = _et_find(props_node, 'md:RegisterType') + meta['RegisterType'] = rt_node.text if rt_node is not None and rt_node.text else 'Balances' + elif obj_type == 'ChartOfCharacteristicTypes': + 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 + meta['HasValueType'] = True + elif obj_type == 'ExchangePlan': + 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 + meta['Hierarchical'] = False + meta['HierarchyType'] = None + meta['Owners'] = [] + elif obj_type == 'ChartOfAccounts': + 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 + meta['Hierarchical'] = 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' + meta['Owners'] = [] + max_ed_node = _et_find(props_node, 'md:MaxExtDimensionCount') + meta['MaxExtDimensionCount'] = int(max_ed_node.text) if max_ed_node is not None and max_ed_node.text else 0 + meta['AccountingFlags'] = extract_fields(child_objs, 'AccountingFlag') + meta['ExtDimensionAccountingFlags'] = extract_fields(child_objs, 'ExtDimensionAccountingFlag') + + 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'}, + }, + # --- Register defaults --- + 'informationRegister.record': { + 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + 'informationRegister.list': { + 'columns': 'all', 'columnType': 'labelField', + 'tableCommandBar': 'none', 'commandBar': 'auto', + 'properties': {}, + }, + 'accumulationRegister.list': { + 'columns': 'all', 'columnType': 'labelField', + 'tableCommandBar': 'none', 'commandBar': 'auto', + 'properties': {}, + }, + # --- Catalog-like type defaults --- + 'chartOfCharacteristicTypes.item': {'basedOn': 'catalog.item'}, + 'chartOfCharacteristicTypes.folder': {'basedOn': 'catalog.folder'}, + 'chartOfCharacteristicTypes.list': {'basedOn': 'catalog.list'}, + 'chartOfCharacteristicTypes.choice': {'basedOn': 'catalog.choice'}, + 'exchangePlan.item': {'basedOn': 'catalog.item'}, + 'exchangePlan.list': {'basedOn': 'catalog.list'}, + 'exchangePlan.choice': {'basedOn': 'catalog.choice'}, + # --- ChartOfAccounts defaults --- + 'chartOfAccounts.item': { + 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, + 'fieldDefaults': {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}}, + 'properties': {}, + }, + 'chartOfAccounts.folder': { + 'parent': {'title': '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443'}, + 'properties': {'windowOpeningMode': 'LockOwnerWindow'}, + }, + 'chartOfAccounts.list': {'basedOn': 'catalog.list'}, + 'chartOfAccounts.choice': {'basedOn': 'catalog.choice'}, + } + + # 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 + + +# Non-displayable types — cannot be bound to form elements +NON_DISPLAYABLE_TYPES = ('ValueStorage', 'v8:ValueStorage', 'ХранилищеЗначения') + +def is_displayable_type(type_str): + return not any(nd in type_str for nd in NON_DISPLAYABLE_TYPES) + +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 + + # (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике. + # Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы + # from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.) + + # 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']: + if not is_displayable_type(attr['Type']): + continue + 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'), ('userVisible', 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 + if not is_displayable_type(attr['Type']): + 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 = [] + # Standard columns: Number + Date + columns.append(OrderedDict([('labelField', 'Номер'), ('path', 'Список.Number')])) + columns.append(OrderedDict([('labelField', 'Дата'), ('path', 'Список.Date')])) + # All custom attributes as labelField + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + 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'), ('userVisible', 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), + ]) + + 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 and is_displayable_type(attr['Type'])] + + # 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)]) + ]), + ]) + + +# --- InformationRegister DSL generators --- + +def generate_information_register_dsl(meta, preset_data, purpose): + p_key = f"informationRegister.{purpose.lower()}" + p = preset_data.get(p_key, {}) + fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} + dispatch = { + 'Record': lambda: generate_information_register_record_dsl(meta, p, fd), + 'List': lambda: generate_information_register_list_dsl(meta, p), + } + return dispatch[purpose]() + + +def generate_information_register_record_dsl(meta, p, fd): + elements = OrderedDict() + is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' + + # Period first (if periodic) + if is_periodic: + elements['\u041f\u0435\u0440\u0438\u043e\u0434'] = {'element': 'input', 'path': '\u0417\u0430\u043f\u0438\u0441\u044c.Period'} + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + elements[dim['Name']] = new_field_element(dim['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{dim['Name']}", dim['Type'], fd) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + elements[res['Name']] = new_field_element(res['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{res['Name']}", res['Type'], fd) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + elements[attr['Name']] = new_field_element(attr['Name'], f"\u0417\u0430\u043f\u0438\u0441\u044c.{attr['Name']}", attr['Type'], fd) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u0417\u0430\u043f\u0438\u0441\u044c', 'type': f"InformationRegisterRecordManager.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_information_register_list_dsl(meta, p): + is_periodic = meta.get('Periodicity') and meta['Periodicity'] != 'Nonperiodical' + is_recorder_subordinate = meta.get('WriteMode') == 'RecorderSubordinate' + + columns_list = [] + # Period + if is_periodic: + columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) + # Recorder/LineNumber for subordinate registers + if is_recorder_subordinate: + columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) + columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + + 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_list), + ]) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', [table_el]), + ('attributes', [ + {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"InformationRegister.{meta['Name']}", 'dynamicDataRead': True}} + ]), + ]) + + +# --- AccumulationRegister DSL generators --- + +def generate_accumulation_register_dsl(meta, preset_data, purpose): + p_key = f"accumulationRegister.{purpose.lower()}" + p = preset_data.get(p_key, {}) + dispatch = { + 'List': lambda: generate_accumulation_register_list_dsl(meta, p), + } + return dispatch[purpose]() + + +def generate_accumulation_register_list_dsl(meta, p): + columns_list = [] + # AccumulationRegisters always have Period, Recorder, LineNumber + columns_list.append(OrderedDict([('labelField', '\u041f\u0435\u0440\u0438\u043e\u0434'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Period')])) + columns_list.append(OrderedDict([('labelField', '\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.Recorder')])) + columns_list.append(OrderedDict([('labelField', '\u041d\u043e\u043c\u0435\u0440\u0421\u0442\u0440\u043e\u043a\u0438'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a.LineNumber')])) + # Dimensions + for dim in meta.get('Dimensions', []): + if not is_displayable_type(dim['Type']): + continue + columns_list.append(OrderedDict([('labelField', dim['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{dim['Name']}")])) + # Resources + for res in meta.get('Resources', []): + if not is_displayable_type(res['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', res['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, res['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{res['Name']}")])) + # Attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + el_key = 'check' if re.match(r'^xs:boolean$|^Boolean$', attr['Type']) else 'labelField' + columns_list.append(OrderedDict([(el_key, attr['Name']), ('path', f"\u0421\u043f\u0438\u0441\u043e\u043a.{attr['Name']}")])) + + 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_list), + ]) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', [table_el]), + ('attributes', [ + {'name': '\u0421\u043f\u0438\u0441\u043e\u043a', 'type': 'DynamicList', 'main': True, 'settings': {'mainTable': f"AccumulationRegister.{meta['Name']}", 'dynamicDataRead': True}} + ]), + ]) + + +# --- ChartOfCharacteristicTypes (delegates to Catalog) --- + +def generate_chart_of_characteristic_types_dsl(meta, preset_data, purpose): + # Delegate to Catalog generators -- meta already has CodeLength, DescriptionLength, etc. + dsl = generate_catalog_dsl(meta, preset_data, purpose) + + # Post-patch: replace Catalog types with ChartOfCharacteristicTypes types + cat_obj_type = f"CatalogObject.{meta['Name']}" + ccoct_obj_type = f"ChartOfCharacteristicTypesObject.{meta['Name']}" + cat_list_type = f"Catalog.{meta['Name']}" + ccoct_list_type = f"ChartOfCharacteristicTypes.{meta['Name']}" + + for a in dsl['attributes']: + if a.get('type') == cat_obj_type: + a['type'] = ccoct_obj_type + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: + a['settings']['mainTable'] = ccoct_list_type + + # For Item forms: inject ValueType field after Description/ГруппаКодНаименование + if purpose == 'Item' and dsl.get('elements'): + vt_el = OrderedDict([('input', '\u0422\u0438\u043f\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ValueType')]) + els = dsl['elements'] + if isinstance(els, list): + inserted = False + new_els = [] + for el in els: + new_els.append(el) + if not inserted and isinstance(el, dict): + name = el.get('input') or el.get('group') or '' + if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): + new_els.append(vt_el) + inserted = True + if not inserted: + new_els.append(vt_el) + dsl['elements'] = new_els + + return dsl + + +# --- ExchangePlan (delegates to Catalog) --- + +def generate_exchange_plan_dsl(meta, preset_data, purpose): + # ExchangePlans are not hierarchical and have no Folder form + dsl = generate_catalog_dsl(meta, preset_data, purpose) + + # Post-patch: replace Catalog types with ExchangePlan types + cat_obj_type = f"CatalogObject.{meta['Name']}" + ep_obj_type = f"ExchangePlanObject.{meta['Name']}" + cat_list_type = f"Catalog.{meta['Name']}" + ep_list_type = f"ExchangePlan.{meta['Name']}" + + for a in dsl['attributes']: + if a.get('type') == cat_obj_type: + a['type'] = ep_obj_type + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == cat_list_type: + a['settings']['mainTable'] = ep_list_type + + # For Item forms: inject SentNo, ReceivedNo after Code/Description + if purpose == 'Item' and dsl.get('elements'): + sent_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.SentNo'), ('readOnly', True)]) + recv_el = OrderedDict([('input', '\u041d\u043e\u043c\u0435\u0440\u041f\u0440\u0438\u043d\u044f\u0442\u043e\u0433\u043e'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ReceivedNo'), ('readOnly', True)]) + els = dsl['elements'] + if isinstance(els, list): + inserted = False + new_els = [] + for el in els: + new_els.append(el) + if not inserted and isinstance(el, dict): + name = el.get('input') or el.get('group') or '' + if name in ('\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', '\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u0434\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'): + new_els.append(sent_el) + new_els.append(recv_el) + inserted = True + if not inserted: + new_els.append(sent_el) + new_els.append(recv_el) + dsl['elements'] = new_els + + return dsl + + +# --- ChartOfAccounts DSL generators --- + +def generate_chart_of_accounts_dsl(meta, preset_data, purpose): + p_key = f"chartOfAccounts.{purpose.lower()}" + p = preset_data.get(p_key, {}) + fd = p.get('fieldDefaults') or {'ref': {'choiceButton': True}, 'boolean': {'element': 'check'}} + dispatch = { + 'Item': lambda: generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data), + 'Folder': lambda: generate_chart_of_accounts_folder_dsl(meta, p), + 'List': lambda: generate_chart_of_accounts_list_dsl(meta, preset_data), + 'Choice': lambda: generate_chart_of_accounts_choice_dsl(meta, preset_data), + } + return dispatch[purpose]() + + +def generate_chart_of_accounts_item_dsl(meta, p, fd, preset_data): + elements = [] + + # Header: Code + Parent + header_left_children = [] + if meta.get('CodeLength', 0) > 0: + header_left_children.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + header_right_children = [] + if meta.get('Hierarchical'): + parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') + header_right_children.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) + + if len(header_right_children) > 0: + elements.append(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', header_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', header_right_children)]), + ]), + ])) + elif len(header_left_children) > 0: + elements.extend(header_left_children) + + # Description + if meta.get('DescriptionLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + + # OffBalance + elements.append(OrderedDict([('check', '\u0417\u0430\u0431\u0430\u043b\u0430\u043d\u0441\u043e\u0432\u044b\u0439'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.OffBalance')])) + + # AccountingFlags as checkboxes + if meta.get('AccountingFlags') and len(meta['AccountingFlags']) > 0: + flag_children = [] + for flag in meta['AccountingFlags']: + flag_children.append(OrderedDict([('check', flag['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{flag['Name']}")])) + elements.append(OrderedDict([ + ('group', 'vertical'), ('name', '\u0413\u0440\u0443\u043f\u043f\u0430\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438\u0423\u0447\u0435\u0442\u0430'), ('title', '\u041f\u0440\u0438\u0437\u043d\u0430\u043a\u0438 \u0443\u0447\u0435\u0442\u0430'), + ('children', flag_children), + ])) + + # ExtDimensionTypes table + if meta.get('MaxExtDimensionCount', 0) > 0: + # Column names are prefixed with the table name (like the generic TS path and stock 1C), + # else a subconto flag column collides with a same-named account accounting-flag checkbox. + ed_table = '\u0412\u0438\u0434\u044b\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e' + ed_cols = [] + ed_cols.append(OrderedDict([('input', f"{ed_table}\u0412\u0438\u0434\u0421\u0443\u0431\u043a\u043e\u043d\u0442\u043e"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.ExtDimensionType')])) + ed_cols.append(OrderedDict([('check', f"{ed_table}\u0422\u043e\u043b\u044c\u043a\u043e\u041e\u0431\u043e\u0440\u043e\u0442\u044b"), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.TurnoversOnly')])) + if meta.get('ExtDimensionAccountingFlags'): + for ed_flag in meta['ExtDimensionAccountingFlags']: + ed_cols.append(OrderedDict([('check', f"{ed_table}{ed_flag['Name']}"), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes.{ed_flag['Name']}")])) + elements.append(OrderedDict([ + ('table', ed_table), + ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.ExtDimensionTypes'), + ('columns', ed_cols), + ])) + + # Custom attributes + for attr in meta['Attributes']: + if not is_displayable_type(attr['Type']): + continue + elements.append(new_field_element(attr['Name'], f"\u041e\u0431\u044a\u0435\u043a\u0442.{attr['Name']}", attr['Type'], fd)) + + # 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'] + for ts in meta['TabularSections']: + if ts['Name'] in ts_exclude: + continue + ts_cols = [] + for col in ts['Columns']: + if not is_displayable_type(col['Type']): + continue + 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)) + elements.append(OrderedDict([('table', ts['Name']), ('path', f"\u041e\u0431\u044a\u0435\u043a\u0442.{ts['Name']}"), ('columns', ts_cols)])) + + props = OrderedDict() + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_chart_of_accounts_folder_dsl(meta, p): + elements = [] + if meta.get('CodeLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041a\u043e\u0434'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Code')])) + if meta.get('DescriptionLength', 0) > 0: + elements.append(OrderedDict([('input', '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Description')])) + if meta.get('Hierarchical'): + parent_title = (p.get('parent') or {}).get('title', '\u041f\u043e\u0434\u0447\u0438\u043d\u0435\u043d \u0441\u0447\u0435\u0442\u0443') + elements.append(OrderedDict([('input', '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c'), ('path', '\u041e\u0431\u044a\u0435\u043a\u0442.Parent'), ('title', parent_title)])) + + props = OrderedDict([('windowOpeningMode', 'LockOwnerWindow')]) + if p.get('properties'): + for k in p['properties']: + props[k] = p['properties'][k] + + return OrderedDict([ + ('title', meta['Synonym']), + ('useForFoldersAndItems', 'Folders'), + ('properties', props), + ('elements', elements), + ('attributes', [ + {'name': '\u041e\u0431\u044a\u0435\u043a\u0442', 'type': f"ChartOfAccountsObject.{meta['Name']}", 'main': True, 'savedData': True} + ]), + ]) + + +def generate_chart_of_accounts_list_dsl(meta, preset_data): + # Delegate to Catalog List and patch types + dsl = generate_catalog_dsl(meta, preset_data, 'List') + for a in dsl['attributes']: + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": + a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" + return dsl + + +def generate_chart_of_accounts_choice_dsl(meta, preset_data): + dsl = generate_catalog_dsl(meta, preset_data, 'Choice') + for a in dsl['attributes']: + if a.get('type') == 'DynamicList' and a.get('settings') and a['settings'].get('mainTable') == f"Catalog.{meta['Name']}": + a['settings']['mainTable'] = f"ChartOfAccounts.{meta['Name']}" + return dsl + + +# ═══════════════════════════════════════════════════════════════════════════ +# END OF FROM-OBJECT MODE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════ + + +def esc_xml(s): + # Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > . + # Кавычки/апострофы в тексте 1С не экранирует (пишет литерально) — " ломал бы раундтрип. + return s.replace('&', '&').replace('<', '<').replace('>', '>') + + +def di_attr(el): + # DisplayImportance — атрибут открывающего тега элемента (адаптивная важность). "" если нет. + if isinstance(el, dict) and el.get('displayImportance'): + return f' DisplayImportance="{esc_xml(str(el["displayImportance"]))}"' + return '' + + +# Базовая директория для @file-ссылок в query динсписка (устанавливается в main) +QUERY_BASE_DIR = None + + +def resolve_query_value(val, base_dir): + if not val.startswith('@'): + return val + file_path = val[1:] + if os.path.isabs(file_path): + candidates = [file_path] + else: + candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)] + for c in candidates: + if os.path.exists(c): + with open(c, 'r', encoding='utf-8-sig') as f: + return f.read().rstrip() + print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + sys.exit(1) + + +def emit_ml_items(lines, indent, val): + # строка → один ru-элемент; объект {lang: text} → по элементу на язык + if isinstance(val, dict): + for k, v in val.items(): + lines.append(f"{indent}<v8:item>") + lines.append(f"{indent}\t<v8:lang>{k}</v8:lang>") + lines.append(f"{indent}\t<v8:content>{esc_xml(str(v))}</v8:content>") + lines.append(f"{indent}</v8:item>") + else: + lines.append(f"{indent}<v8:item>") + lines.append(f"{indent}\t<v8:lang>ru</v8:lang>") + lines.append(f"{indent}\t<v8:content>{esc_xml(str(val))}</v8:content>") + lines.append(f"{indent}</v8:item>") + + +def emit_mltext(lines, indent, tag, text, xsi_type=None): + attr = f' xsi:type="{xsi_type}"' if xsi_type else '' + if not text: + lines.append(f"{indent}<{tag}{attr}/>") + return + lines.append(f"{indent}<{tag}{attr}>") + emit_ml_items(lines, f"{indent}\t", text) + lines.append(f"{indent}</{tag}>") + + +def emit_us_presentation(lines, indent, tag, val): + # <dcsset:userSettingPresentation>: плоская строка → xsi:type="xs:string"; мультиязычный → v8:LocalStringType + if val is None: + return + if isinstance(val, str): + lines.append(f'{indent}<{tag} xsi:type="xs:string">{esc_xml(val)}</{tag}>') + else: + emit_mltext(lines, indent, tag, val, xsi_type='v8:LocalStringType') + + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + + +def new_uuid(): + return str(uuid.uuid4()) + + +# ───────────────────────────────────────────────────────────────────────────── +# Настройки компоновщика ListSettings: filter/order/conditionalAppearance. +# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны). +# ───────────────────────────────────────────────────────────────────────────── +COMPARISON_TYPES = { + '=': 'Equal', '<>': 'NotEqual', + '>': 'Greater', '>=': 'GreaterOrEqual', + '<': 'Less', '<=': 'LessOrEqual', + 'in': 'InList', 'notIn': 'NotInList', + 'inHierarchy': 'InHierarchy', 'inListByHierarchy': 'InListByHierarchy', + 'contains': 'Contains', 'notContains': 'NotContains', + 'beginsWith': 'BeginsWith', 'notBeginsWith': 'NotBeginsWith', + 'like': 'Like', 'notLike': 'NotLike', + 'подобно': 'Like', 'неподобно': 'NotLike', # рус. синоним + 'filled': 'Filled', 'notFilled': 'NotFilled', +} +# Регистронезависимый лукап (зеркало PS-хэша): Like/LIKE/ПОДОБНО → канон +_COMPARISON_TYPES_CI = {k.lower(): v for k, v in COMPARISON_TYPES.items()} + +_REF_TYPE_RE = re.compile( + r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|' + r'БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|' + r'ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|' + r'InformationRegister|ExchangePlan)\.') + + +def parse_filter_shorthand(s): + result = {'field': '', 'op': 'Equal', 'value': None, 'use': True, + 'userSettingID': None, 'viewMode': None, 'presentation': None} + if re.search(r'@user', s): + result['userSettingID'] = 'auto' + s = re.sub(r'\s*@user', '', s) + if re.search(r'@off', s): + result['use'] = False + s = re.sub(r'\s*@off', '', s) + if re.search(r'@quickAccess', s): + result['viewMode'] = 'QuickAccess' + s = re.sub(r'\s*@quickAccess', '', s) + if re.search(r'@normal', s): + result['viewMode'] = 'Normal' + s = re.sub(r'\s*@normal', '', s) + if re.search(r'@inaccessible', s): + result['viewMode'] = 'Inaccessible' + s = re.sub(r'\s*@inaccessible', '', s) + s = s.strip() + op_patterns = ['<>', '>=', '<=', '=', '>', '<', + r'notIn\b', r'in\b', r'inHierarchy\b', r'inListByHierarchy\b', + r'notContains\b', r'contains\b', r'notBeginsWith\b', r'beginsWith\b', + r'notLike\b', r'like\b', r'неподобно\b', r'подобно\b', + r'notFilled\b', r'filled\b'] + op_joined = '|'.join(op_patterns) + m = re.match(r'^(.+?)\s+(' + op_joined + r')\s*(.*)?$', s, re.IGNORECASE) + if m: + result['field'] = m.group(1).strip() + result['op'] = m.group(2).strip() + val_part = m.group(3).strip() if m.group(3) else '' + if val_part and val_part != '_': + if val_part == 'true' or val_part == 'false': + result['value'] = (val_part == 'true') + result['valueType'] = 'xs:boolean' + elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_part): + # дата без valueType → emit_filter_item выведет StandardBeginningDate Custom (дефолт даты в фильтре) + result['value'] = val_part + elif re.match(r'^\d+(\.\d+)?$', val_part): + result['value'] = val_part + result['valueType'] = 'xs:decimal' + elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', val_part): + result['value'] = val_part + result['valueType'] = 'dcscor:DesignTimeValue' + else: + result['value'] = val_part + result['valueType'] = 'xs:string' + else: + result['field'] = s + return result + + +def _value_type_for(v, explicit=None): + if explicit: + return explicit + if isinstance(v, bool): + return 'xs:boolean' + if isinstance(v, (int, float)): + return 'xs:decimal' + vs = str(v) + if re.match(r'^\d{4}-\d{2}-\d{2}T', vs): + return 'xs:dateTime' + if re.match(r'^-?\d+(\.\d+)?$', vs): + return 'xs:decimal' + if _REF_TYPE_RE.match(vs): + return 'dcscor:DesignTimeValue' + return 'xs:string' + + +def emit_filter_item(lines, item, indent): + if item.get('group'): + g = str(item['group']) + group_type = {'And': 'AndGroup', 'Or': 'OrGroup', 'Not': 'NotGroup'}.get(g, g + 'Group') + lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemGroup">') + lines.append(f'{indent}\t<dcsset:groupType>{group_type}</dcsset:groupType>') + if item.get('items'): + for sub in item['items']: + if isinstance(sub, str): + parsed = parse_filter_shorthand(sub) + obj = {'field': parsed['field'], 'op': parsed['op']} + if parsed['use'] is False: + obj['use'] = False + if parsed['value'] is not None: + obj['value'] = parsed['value'] + if parsed.get('valueType'): + obj['valueType'] = parsed['valueType'] + if parsed.get('userSettingID'): + obj['userSettingID'] = parsed['userSettingID'] + if parsed.get('viewMode'): + obj['viewMode'] = parsed['viewMode'] + sub = obj + emit_filter_item(lines, sub, f'{indent}\t') + if item.get('presentation'): + emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) + if item.get('viewMode'): + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') + if item.get('userSettingID'): + guid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(guid)}</dcsset:userSettingID>') + if item.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) + lines.append(f'{indent}</dcsset:item>') + return + + lines.append(f'{indent}<dcsset:item xsi:type="dcsset:FilterItemComparison">') + if item.get('use') is False: + lines.append(f'{indent}\t<dcsset:use>false</dcsset:use>') + lines.append(f'{indent}\t<dcsset:left xsi:type="dcscor:Field">{esc_xml(str(item.get("field", "")))}</dcsset:left>') + # Регистронезависимый лукап (зеркало PS): Like/LIKE/ПОДОБНО → канон; иначе — как есть + comp_type = _COMPARISON_TYPES_CI.get(str(item.get('op')).lower()) + if not comp_type: + comp_type = str(item.get('op')) + lines.append(f'{indent}\t<dcsset:comparisonType>{esc_xml(comp_type)}</dcsset:comparisonType>') + val = item.get('value') + if isinstance(val, list): + if len(val) == 0: + lines.append(f'{indent}\t<dcsset:right xsi:type="v8:ValueListType">') + lines.append(f'{indent}\t\t<v8:valueType/>') + lines.append(f'{indent}\t\t<v8:lastId xsi:type="xs:decimal">-1</v8:lastId>') + lines.append(f'{indent}\t</dcsset:right>') + else: + for v in val: + vt = _value_type_for(v, item.get('valueType')) + v_str = str(v).lower() if isinstance(v, bool) else esc_xml(str(v)) + lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>') + elif val is not None and ( + re.search(r'Standard(Beginning|End)Date$', str(item.get('valueType') or '')) or + (not item.get('valueType') and isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val))): + # Стандартная дата начала/окончания. Формы: объект {variant, date?} (Custom несёт <v8:date>); + # строка-вариант "BeginningOfThisDay" (именованный без даты); голая ISO-дата без valueType — + # шорткат для Custom+date (дата в фильтре почти всегда SBD Custom, корпус 268 vs 2 xs:dateTime). + sd_type = re.sub(r'^v8:', '', str(item['valueType'])) if item.get('valueType') else 'StandardBeginningDate' + if isinstance(val, dict): + variant = str(val.get('variant', '')); date_v = val.get('date') + elif isinstance(val, str) and re.match(r'^\d{4}-\d{2}-\d{2}T', val): + variant = 'Custom'; date_v = val + else: + variant = str(val); date_v = None + lines.append(f'{indent}\t<dcsset:right xsi:type="v8:{sd_type}">') + lines.append(f'{indent}\t\t<v8:variant xsi:type="v8:{sd_type}Variant">{esc_xml(variant)}</v8:variant>') + if date_v is not None: + lines.append(f'{indent}\t\t<v8:date>{esc_xml(str(date_v))}</v8:date>') + lines.append(f'{indent}\t</dcsset:right>') + elif str(val) == '_': + # "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right> + # (напр. <dcsset:right xsi:type="dcscor:Field"/> — сравнение с незаданным полем). + vt = str(item['valueType']) if item.get('valueType') else 'xs:string' + lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}"/>') + elif val is not None: + vt = _value_type_for(val, item.get('valueType')) + v_str = str(val).lower() if isinstance(val, bool) else esc_xml(str(val)) + lines.append(f'{indent}\t<dcsset:right xsi:type="{vt}">{v_str}</dcsset:right>') + if item.get('presentation'): + emit_mltext(lines, f'{indent}\t', 'dcsset:presentation', item['presentation']) + if item.get('viewMode'): + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') + if item.get('userSettingID'): + uid = new_uuid() if str(item['userSettingID']) == 'auto' else str(item['userSettingID']) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if item.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t', 'dcsset:userSettingPresentation', item['userSettingPresentation']) + lines.append(f'{indent}</dcsset:item>') + + +def emit_filter(lines, items, indent, block_view_mode=None, block_user_setting_id=None): + has_items = bool(items) and len(items) > 0 + has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) + if not has_items and not has_block_meta: + return + lines.append(f'{indent}<dcsset:filter>') + for item in (items or []): + if isinstance(item, str): + parsed = parse_filter_shorthand(item) + obj = {'field': parsed['field'], 'op': parsed['op']} + if parsed['use'] is False: + obj['use'] = False + if parsed['value'] is not None: + obj['value'] = parsed['value'] + if parsed.get('valueType'): + obj['valueType'] = parsed['valueType'] + if parsed.get('userSettingID'): + obj['userSettingID'] = parsed['userSettingID'] + if parsed.get('viewMode'): + obj['viewMode'] = parsed['viewMode'] + emit_filter_item(lines, obj, f'{indent}\t') + else: + emit_filter_item(lines, item, f'{indent}\t') + if block_view_mode is not None: + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') + if block_user_setting_id is not None: + uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + lines.append(f'{indent}</dcsset:filter>') + + +def emit_order(lines, items, indent, skip_auto=False, block_view_mode=None, block_user_setting_id=None): + has_items = bool(items) and len(items) > 0 + has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) + if not has_items and not has_block_meta: + return + lines.append(f'{indent}<dcsset:order>') + for item in (items or []): + if isinstance(item, str): + if item == 'Auto': + if not skip_auto: + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>') + else: + parts = re.split(r'\s+', item) + field = parts[0] + direction = 'Asc' + if len(parts) > 1 and re.match(r'(?i)^(desc|убыв)', parts[1]): + direction = 'Desc' + elif len(parts) > 1 and re.match(r'(?i)^(asc|возр)', parts[1]): + direction = 'Asc' + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">') + lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(field)}</dcsset:field>') + lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>') + lines.append(f'{indent}\t</dcsset:item>') + else: + if item.get('field') == 'Auto' or item.get('type') == 'auto': + if not skip_auto: + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemAuto"/>') + continue + direction = str(item['direction']) if item.get('direction') else 'Asc' + if re.match(r'(?i)^(desc|убыв)', direction): + direction = 'Desc' + elif re.match(r'(?i)^(asc|возр)', direction): + direction = 'Asc' + lines.append(f'{indent}\t<dcsset:item xsi:type="dcsset:OrderItemField">') + if item.get('use') is False: + lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>') + lines.append(f'{indent}\t\t<dcsset:field>{esc_xml(str(item.get("field", "")))}</dcsset:field>') + lines.append(f'{indent}\t\t<dcsset:orderType>{direction}</dcsset:orderType>') + if item.get('viewMode'): + lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(item["viewMode"]))}</dcsset:viewMode>') + lines.append(f'{indent}\t</dcsset:item>') + if block_view_mode is not None: + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') + if block_user_setting_id is not None: + uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + lines.append(f'{indent}</dcsset:order>') + + +def emit_appearance_value(lines, key, val, indent): + lines.append(f'{indent}<dcscor:item xsi:type="dcsset:SettingsParameterValue">') + + def _has_key(o, k): + return isinstance(o, dict) and (k in o) + + def _get(o, k): + return o.get(k) if isinstance(o, dict) else None + + is_top_level_line = _has_key(val, '@type') and (str(_get(val, '@type')) == 'Line') + use_wrapper = False + inner_val = val + nested_items = None + if is_top_level_line: + if _has_key(val, 'use') and (_get(val, 'use') is False): + use_wrapper = True + if _has_key(val, 'items'): + nested_items = _get(val, 'items') + elif _has_key(val, 'value') and isinstance(val, dict): + inner_val = _get(val, 'value') + if _has_key(val, 'use') and (_get(val, 'use') is False): + use_wrapper = True + if _has_key(val, 'items'): + nested_items = _get(val, 'items') + if use_wrapper: + lines.append(f'{indent}\t<dcscor:use>false</dcscor:use>') + lines.append(f'{indent}\t<dcscor:parameter>{esc_xml(key)}</dcscor:parameter>') + + is_font_dict = isinstance(inner_val, dict) and inner_val.get('@type') is not None and str(inner_val.get('@type')) == 'Font' + is_line_dict = _has_key(inner_val, '@type') and (str(_get(inner_val, '@type')) == 'Line') + is_dict = isinstance(inner_val, dict) + if is_line_dict: + lw = _get(inner_val, 'width') if _has_key(inner_val, 'width') else 0 + lg = ('true' if _get(inner_val, 'gap') else 'false') if _has_key(inner_val, 'gap') else 'false' + ls = str(_get(inner_val, 'style')) if _has_key(inner_val, 'style') else 'None' + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Line" width="{lw}" gap="{lg}">') + lines.append(f'{indent}\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">{esc_xml(ls)}</v8ui:style>') + lines.append(f'{indent}\t</dcscor:value>') + elif is_font_dict: + attr_parts = [] + for attr_name in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + if attr_name in inner_val: + av = inner_val[attr_name] + if av is not None: + attr_parts.append(f'{attr_name}="{esc_xml(str(av))}"') + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Font" {" ".join(attr_parts)}/>') + elif is_dict and _has_key(inner_val, 'field'): + # Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки + lines.append(f'{indent}\t<dcscor:value xsi:type="dcscor:Field">{esc_xml(str(_get(inner_val, "field")))}</dcscor:value>') + elif is_dict: + # Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value + emit_mltext(lines, f'{indent}\t', 'dcscor:value', inner_val, xsi_type='v8:LocalStringType') + else: + actual_val = str(inner_val) + key_type_map = { + 'Размещение': 'dcscor:DataCompositionTextPlacementType', + 'ГоризонтальноеПоложение': 'v8ui:HorizontalAlign', + 'ВертикальноеПоложение': 'v8ui:VerticalAlign', + 'ОриентацияТекста': 'xs:decimal', + 'РасположениеИтогов': 'dcscor:DataCompositionTotalPlacement', + 'ТипМакета': 'dcsset:DataCompositionGroupTemplateType', + } + key_type = key_type_map.get(key) + if key_type: + lines.append(f'{indent}\t<dcscor:value xsi:type="{key_type}">{esc_xml(actual_val)}</dcscor:value>') + elif re.match(r'^(style|web|win):', actual_val): + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>') + elif actual_val == 'true' or actual_val == 'false': + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:boolean">{actual_val}</dcscor:value>') + elif key == 'Текст' or key == 'Заголовок' or key == 'Формат': + # Голая строка = плоский xs:string (нелокализованный литерал). Локализуемый → объект {ru,en}. + # Пустая строка → самозакрывающийся тег (как у платформы). + if actual_val == '': + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string"/>') + else: + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>') + elif re.match(r'^-?\d+(\.\d+)?$', actual_val): + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:decimal">{actual_val}</dcscor:value>') + elif key == 'ЦветТекста' or key == 'ЦветФона' or key == 'ЦветГраницы': + lines.append(f'{indent}\t<dcscor:value xsi:type="v8ui:Color">{esc_xml(actual_val)}</dcscor:value>') + else: + lines.append(f'{indent}\t<dcscor:value xsi:type="xs:string">{esc_xml(actual_val)}</dcscor:value>') + if nested_items: + if isinstance(nested_items, dict): + for nk, nv in nested_items.items(): + emit_appearance_value(lines, nk, nv, f'{indent}\t') + lines.append(f'{indent}</dcscor:item>') + + +def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance'): + has_items = bool(items) and len(items) > 0 + has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) + if not has_items and not has_block_meta: + return + lines.append(f'{indent}<{wrap_tag}>') + for ca in (items or []): + lines.append(f'{indent}\t<dcsset:item>') + if ca.get('use') is False: + lines.append(f'{indent}\t\t<dcsset:use>false</dcsset:use>') + if ca.get('selection') and len(ca['selection']) > 0: + lines.append(f'{indent}\t\t<dcsset:selection>') + for sel in ca['selection']: + lines.append(f'{indent}\t\t\t<dcsset:item>') + lines.append(f'{indent}\t\t\t\t<dcsset:field>{esc_xml(str(sel))}</dcsset:field>') + lines.append(f'{indent}\t\t\t</dcsset:item>') + lines.append(f'{indent}\t\t</dcsset:selection>') + else: + lines.append(f'{indent}\t\t<dcsset:selection/>') + if ca.get('filter') and len(ca['filter']) > 0: + emit_filter(lines, ca['filter'], f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t<dcsset:filter/>') + if ca.get('appearance'): + lines.append(f'{indent}\t\t<dcsset:appearance>') + for k, v in ca['appearance'].items(): + emit_appearance_value(lines, k, v, f'{indent}\t\t\t') + lines.append(f'{indent}\t\t</dcsset:appearance>') + if ca.get('presentation'): + if isinstance(ca['presentation'], dict): + # Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation) + lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="v8:LocalStringType">') + emit_ml_items(lines, f'{indent}\t\t\t', ca['presentation']) + lines.append(f'{indent}\t\t</dcsset:presentation>') + else: + lines.append(f'{indent}\t\t<dcsset:presentation xsi:type="xs:string">{esc_xml(str(ca["presentation"]))}</dcsset:presentation>') + if ca.get('viewMode'): + lines.append(f'{indent}\t\t<dcsset:viewMode>{esc_xml(str(ca["viewMode"]))}</dcsset:viewMode>') + if ca.get('userSettingID'): + uid = new_uuid() if str(ca['userSettingID']) == 'auto' else str(ca['userSettingID']) + lines.append(f'{indent}\t\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + if ca.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', ca['userSettingPresentation']) + if ca.get('useInDontUse') and len(ca['useInDontUse']) > 0: + use_in_order = ['group', 'hierarchicalGroup', 'overall', 'fieldsHeader', 'header', + 'parameters', 'filter', 'resourceFieldsHeader', 'overallHeader', + 'overallResourceFieldsHeader'] + sset = {str(n): True for n in ca['useInDontUse']} + for n in use_in_order: + if n in sset: + tag = 'useIn' + n[0].upper() + n[1:] + lines.append(f'{indent}\t\t<dcsset:{tag}>DontUse</dcsset:{tag}>') + lines.append(f'{indent}\t</dcsset:item>') + if block_view_mode is not None: + lines.append(f'{indent}\t<dcsset:viewMode>{esc_xml(str(block_view_mode))}</dcsset:viewMode>') + if block_user_setting_id is not None: + uid = new_uuid() if str(block_user_setting_id) == 'auto' else str(block_user_setting_id) + lines.append(f'{indent}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>') + lines.append(f'{indent}</{wrap_tag}>') + + +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 + + +# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё +# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast. +_seen_element_names = set() # пул имён элементов (глобально по всей форме) + +def _ensure_unique(name, seen, kind): + if name in seen: + print(f"[ERROR] Duplicate {kind} name '{name}' — names must be unique within their collection in a 1C form (set a unique 'name')", file=sys.stderr) + sys.exit(1) + seen.add(name) + + +# --- 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"], + "radio": ["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", "columnGroup", "buttonGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page", + "button", "picture", "picField", "calendar", "cmdBar", "popup", + "showInHeader", + "radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode", + "name", "path", "title", "tooltip", "tooltipRepresentation", "extendedTooltip", + "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", + "events", "on", "handlers", + "selectionMode", "showCurrentDate", "widthInMonths", "heightInMonths", "showMonthsPanel", + "titleLocation", "representation", "width", "height", + "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", + "maxWidth", "maxHeight", + "groupHorizontalAlign", "groupVerticalAlign", "horizontalAlign", + "multiLine", "passwordMode", "choiceButton", "clearButton", + "spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint", + "textEdit", "choiceList", + "wrap", "openButton", "listChoiceMode", "showInHeader", "showInFooter", + "extendedEditMultipleValues", "chooseType", "autoCellHeight", + "choiceButtonRepresentation", "footerHorizontalAlign", "headerHorizontalAlign", + "format", "editFormat", "choiceParameters", "choiceParameterLinks", "typeLink", + "hyperlink", "formatted", + "collapsedTitle", "showTitle", "united", "collapsed", "behavior", + "children", "columns", + "changeRowSet", "changeRowOrder", "autoInsertNewRow", "rowFilter", "header", "footer", + "commandBarLocation", "searchStringLocation", "viewStatusLocation", "searchControlLocation", + "excludedCommands", + "pagesRepresentation", + "type", "command", "commandName", "stdCommand", "defaultButton", "locationInCommandBar", "displayImportance", + "commandBar", "contextMenu", "commandSource", + "src", "valuesPicture", "loadTransparent", "headerPicture", "footerPicture", + "autofill", + "choiceMode", "initialTreeView", "enableDrag", "enableStartDrag", + "rowSelectionMode", "verticalLines", "horizontalLines", + "rowPictureDataPath", "tableAutofill", "heightInTableRows", + "multipleChoice", "searchOnInput", "shortcut", + # dynamic-list table block + "defaultItem", "useAlternationRowColor", "fileDragMode", "autoRefresh", + "autoRefreshPeriod", "choiceFoldersAndItems", "restoreCurrentRow", "showRoot", + "allowRootChoice", "updateOnDataChange", "allowGettingCurrentRowURL", + "userSettingsGroup", "rowsPicture", + # AutoCommandBar-маркер (autofill heuristic) на элементе/таблице + "autoCmdBar", + # дополнения командной панели таблицы (тип-ключи + свойства) + "searchString", "viewStatus", "searchControl", "source", "horizontalLocation", "additions", + # generic-скаляры (pass-through) + "verticalAlign", "throughAlign", "enableContentChange", "pictureSize", "titleHeight", + "childItemsWidth", "showLeftMargin", "cellHyperlink", "viewMode", "verticalScrollBar", + "rowInputMode", "mask", "createButton", "fixingInTable", "verticalSpacing", + # InputField choice-скаляры + "choiceListButton", "quickChoice", "autoChoiceIncomplete", + "choiceForm", "choiceHistoryOnInput", "footerDataPath", "minValue", "maxValue", + # Button — пометка toggle-кнопки + "checked", + # спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры + "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", + "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram", "ganttTable", + "showPercent", "largeStep", "markingStep", "step", + "horizontalScrollBar", "viewScalingMode", "output", "selectionShowMode", "protection", + "edit", "showGrid", "showGroups", "showHeaders", "showRowAndColumnNames", "showCellNames", + "pointerType", "drawingSelectionShowMode", "warningOnEditRepresentation", "markingAppearance", + # report-form контекст (generic-скаляры элементов) + "horizontalSpacing", "representationInContextMenu", "settingsNamedItemDetailedRepresentation", + # хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель + "itemHeight", "dropListWidth", "choiceButtonPicture", "transparentPixel", + # хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы + "titleDataPath", "extendedEdit", "maxRowsCount", "autoMaxRowsCount", "heightControlVariant", + "warningOnEdit", "nonselectedPictureText", "editTextUpdate", "footerText", +} + +# picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка +# у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть. +# pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей +# (<Group>Horizontal</Group>), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт. +TYPE_KEYS = ["columnGroup", "buttonGroup", "pages", "page", "group", "input", "check", "radio", "label", "labelField", "table", + "button", "calendar", "cmdBar", "popup", "searchString", "viewStatus", "searchControl", "picField", "picture", + "spreadsheet", "html", "textDoc", "formattedDoc", "progressBar", "trackBar", + "chart", "ganttChart", "graphicalSchema", "planner", "periodField", "dendrogram"] + +# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio) +ELEMENT_TYPE_SYNONYMS = { + "commandBar": "cmdBar", + "autoCommandBar": "autoCmdBar", + "КоманднаяПанель": "cmdBar", + "InputField": "input", + "ПолеВвода": "input", + "CheckBoxField": "check", + "ПолеФлажка": "check", + "RadioButtonField": "radio", + "ПолеПереключателя": "radio", + "radioButton": "radio", + "PictureField": "picField", + "ПолеКартинки": "picField", + "LabelField": "labelField", + "ПолеНадписи": "labelField", + "CalendarField": "calendar", + "ПолеКалендаря": "calendar", + "LabelDecoration": "label", + "Надпись": "label", + "PictureDecoration": "picture", + "Картинка": "picture", + "UsualGroup": "group", + "Группа": "group", + "ОбычнаяГруппа": "group", + "ColumnGroup": "columnGroup", + "ГруппаКолонок": "columnGroup", + "Pages": "pages", + "ГруппаСтраниц": "pages", + "Page": "page", + "Страница": "page", + "Table": "table", + "Таблица": "table", + "Button": "button", + "Кнопка": "button", + "Popup": "popup", + "ВсплывающееМеню": "popup", + # дополнения командной панели таблицы — forgiving: XML-тег/Type/рус.имя → канон + "SearchStringAddition": "searchString", + "SearchStringRepresentation": "searchString", + "строкаПоиска": "searchString", + "отображениеСтрокиПоиска": "searchString", + "Отображение строки поиска": "searchString", + "ViewStatusAddition": "viewStatus", + "ViewStatusRepresentation": "viewStatus", + "состояниеПросмотра": "viewStatus", + "Состояние просмотра": "viewStatus", + "SearchControlAddition": "searchControl", + "SearchControl": "searchControl", + "управлениеПоиском": "searchControl", + "Управление поиском": "searchControl", + # Спец-поля (документ/датчик) — XML-имя/рус. → канон + "SpreadSheetDocumentField": "spreadsheet", + "ПолеТабличногоДокумента": "spreadsheet", + "HTMLDocumentField": "html", + "ПолеHTMLДокумента": "html", + "TextDocumentField": "textDoc", + "ПолеТекстовогоДокумента": "textDoc", + "FormattedDocumentField": "formattedDoc", + "ПолеФорматированногоДокумента": "formattedDoc", + "ProgressBarField": "progressBar", + "ПолеИндикатора": "progressBar", + "TrackBarField": "trackBar", + "ПолеПолосыРегулирования": "trackBar", + "ChartField": "chart", + "ПолеДиаграммы": "chart", + "GanttChartField": "ganttChart", + "ПолеДиаграммыГанта": "ganttChart", + "GraphicalSchemaField": "graphicalSchema", + "ПолеГрафическойСхемы": "graphicalSchema", + "PlannerField": "planner", + "ПолеПланировщика": "planner", + "PeriodField": "periodField", + "ПолеПериода": "periodField", + "DendrogramField": "dendrogram", + "ПолеДендрограммы": "dendrogram", +} + +# Тип-синонимы, применяемые ТОЛЬКО к строковому значению (имя элемента); объект/массив +# у того же слова — companion-панель (свойство), см. normalize_panel_synonyms. +STR_ONLY_TYPE_SYNONYMS = {"commandBar", "autoCommandBar", "КоманднаяПанель"} + +# Companion-панели как СВОЙСТВА (значение объект/массив): синоним → каноника. +PANEL_SYNONYMS = { + 'commandBar': ['commandBar', 'autoCommandBar', 'AutoCommandBar', 'autoCmdBar', 'cmdBar', 'КоманднаяПанель'], + 'contextMenu': ['contextMenu', 'ContextMenu', 'КонтекстноеМеню'], +} + + +def normalize_panel_synonyms(el): + if not isinstance(el, dict): + return + for canon, syns in PANEL_SYNONYMS.items(): + for syn in syns: + if syn in el and isinstance(el[syn], (list, dict)): + if syn != canon and canon not in el: + el[canon] = el.pop(syn) + break + + +# Maps Russian/English root of typed reference path to canonical English root +REF_ROOT_SYNONYMS = { + "Перечисление": "Enum", + "Справочник": "Catalog", + "Документ": "Document", + "ПланСчетов": "ChartOfAccounts", + "ПланВидовХарактеристик": "ChartOfCharacteristicTypes", + "ПланВидовРасчета": "ChartOfCalculationTypes", + "ПланВидовРасчёта": "ChartOfCalculationTypes", + "ПланОбмена": "ExchangePlan", + "БизнесПроцесс": "BusinessProcess", + "Задача": "Task", + "РегистрСведений": "InformationRegister", + "РегистрНакопления": "AccumulationRegister", + "РегистрБухгалтерии": "AccountingRegister", + "РегистрРасчета": "CalculationRegister", + "РегистрРасчёта": "CalculationRegister", + "ЖурналДокументов": "DocumentJournal", + "КритерийОтбора": "FilterCriterion", +} +ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"} + + +def normalize_meta_type_ref(ref): + # "Справочник.Контрагенты" → "Catalog.Контрагенты"; уже англ — без изменений + if not ref: + return ref + dot = ref.find('.') + if dot < 1: + return ref + root = ref[:dot] + if root in REF_ROOT_SYNONYMS: + return REF_ROOT_SYNONYMS[root] + ref[dot:] + return ref + + +def normalize_choice_value(value): + """Returns dict {xsi_type, text} for a choiceList item value.""" + if isinstance(value, bool): + return {"xsi_type": "xs:boolean", "text": "true" if value else "false"} + if isinstance(value, (int, float)): + return {"xsi_type": "xs:decimal", "text": str(value)} + + s = "" if value is None else str(value) + if not s: + return {"xsi_type": "xs:string", "text": ""} + + # ISO datetime ("2020-01-01T00:00:00") → xs:dateTime + if re.fullmatch(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}', s): + return {"xsi_type": "xs:dateTime", "text": s} + + parts = s.split(".") + if len(parts) >= 2: + root = parts[0] + canon_root = None + if root in REF_ROOT_SYNONYMS: + canon_root = REF_ROOT_SYNONYMS[root] + elif root in REF_ROOT_SYNONYMS.values(): + canon_root = root + + if canon_root: + type_name = parts[1] + normalized = None + if canon_root == "Enum": + if len(parts) == 3 and parts[2] == 'EmptyRef': + # "Enum.X.EmptyRef" — пустая ссылка, НЕ значение перечисления (без .EnumValue.) + normalized = f"Enum.{type_name}.EmptyRef" + elif len(parts) == 3: + normalized = f"Enum.{type_name}.EnumValue.{parts[2]}" + elif len(parts) >= 4: + member = parts[2] + if member in ENUM_VALUE_SYNONYMS: + rest = ".".join(parts[3:]) + else: + rest = ".".join(parts[2:]) + normalized = f"Enum.{type_name}.EnumValue.{rest}" + else: + if len(parts) >= 3: + tail = ".".join(parts[1:]) + normalized = f"{canon_root}.{tail}" + + if normalized: + return {"xsi_type": "xr:DesignTimeRef", "text": normalized} + + return {"xsi_type": "xs:string", "text": s} + + +def emit_choice_presentation(lines, pres, indent): + """Accepts None/empty → <Presentation/>; str → ru only; dict → multi-lang.""" + if pres is None or (isinstance(pres, str) and pres == ""): + lines.append(f"{indent}<Presentation/>") + return + + if isinstance(pres, str): + pairs = [("ru", pres)] + elif isinstance(pres, dict): + pairs = [(str(k), str(v)) for k, v in pres.items()] + else: + pairs = [("ru", str(pres))] + + lines.append(f"{indent}<Presentation>") + for lang, content in pairs: + lines.append(f"{indent}\t<v8:item>") + lines.append(f"{indent}\t\t<v8:lang>{lang}</v8:lang>") + lines.append(f"{indent}\t\t<v8:content>{esc_xml(content)}</v8:content>") + lines.append(f"{indent}\t</v8:item>") + lines.append(f"{indent}</Presentation>") + + +def choice_value_tag(norm): + # <Value> для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы). + if not norm["text"]: + return f'<Value xsi:type="{norm["xsi_type"]}"/>' + return f'<Value xsi:type="{norm["xsi_type"]}">{esc_xml(norm["text"])}</Value>' + + +def emit_choice_list(lines, el, indent): + # <ChoiceList> — у RadioButtonField и InputField. Элемент: { value, presentation?/title? }. + choice_list = el.get('choiceList') or [] + if not choice_list: + return + lines.append(f'{indent}<ChoiceList>') + item_indent = f'{indent}\t' + for item in choice_list: + if not isinstance(item, dict): + continue + val_raw = item.get('value', item.get('значение')) + has_pres = any(k in item for k in ('presentation', 'представление', 'title')) + pres_raw = item.get('presentation', item.get('представление', item.get('title'))) + + # valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) — + # переопределяет авто-детект (normalize_choice_value вывела бы xs:string). + vt_raw = item.get('valueType') + if vt_raw: + norm = {'xsi_type': str(vt_raw), 'text': '' if val_raw is None else str(val_raw)} + else: + norm = normalize_choice_value(val_raw) + + if not has_pres: + if norm['xsi_type'] == 'xr:DesignTimeRef': + tail = norm['text'].split('.')[-1] + pres_raw = title_from_name(tail) + else: + pres_raw = norm['text'] + + lines.append(f'{item_indent}<xr:Item>') + val_indent = f'{item_indent}\t' + lines.append(f'{val_indent}<xr:Presentation/>') + lines.append(f'{val_indent}<xr:CheckState>0</xr:CheckState>') + lines.append(f'{val_indent}<xr:Value xsi:type="FormChoiceListDesTimeValue">') + emit_choice_presentation(lines, pres_raw, f'{val_indent}\t') + lines.append(f'{val_indent}\t{choice_value_tag(norm)}') + lines.append(f'{val_indent}</xr:Value>') + lines.append(f'{item_indent}</xr:Item>') + lines.append(f'{indent}</ChoiceList>') + + +def get_el_prop(obj, names): + # Читает свойство из dict по списку синонимов (первый найденный, иначе None). + if not isinstance(obj, dict): + return None + for n in names: + if n in obj: + return obj[n] + return None + + +def to_scalar_literal(s): + # Литерал shorthand → тип: true/false → bool, целое/дробное → число, иначе строка. + t = str(s).strip() + if t.lower() == 'true': + return True + if t.lower() == 'false': + return False + if re.fullmatch(r'-?\d+', t): + return int(t) + if re.fullmatch(r'-?\d+\.\d+', t): + return float(t) + return t + + +def from_choice_param_shorthand(s): + # "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}. + eq = s.find('=') + if eq < 0: + return {'name': s.strip()} + name = s[:eq].strip() + rest = s[eq + 1:] + if ',' in rest: + return {'name': name, 'value': [to_scalar_literal(p) for p in rest.split(',')]} + return {'name': name, 'value': to_scalar_literal(rest)} + + +def from_choice_param_link_shorthand(s): + # "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}. + eq = s.find('=') + if eq < 0: + return {'name': s.strip()} + o = {'name': s[:eq].strip()} + rest = s[eq + 1:].strip() + m = re.fullmatch(r'(.*):(Clear|DontChange|очистить|неизменять)', rest, re.IGNORECASE) + if m: + o['dataPath'] = m.group(1).strip() + o['valueChange'] = m.group(2) + else: + o['dataPath'] = rest + return o + + +def from_type_link_shorthand(s): + # "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}. + m = re.fullmatch(r'(.*)#(\d+)', str(s)) + if m: + return {'dataPath': m.group(1).strip(), 'linkItem': int(m.group(2))} + return {'dataPath': str(s).strip()} + + +def emit_choice_param_value(lines, value, indent): + # Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): <Presentation/> + <Value>. + # Скаляр → один Value; массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. + lines.append(f'{indent}<Presentation/>') + if isinstance(value, (list, tuple)): + lines.append(f'{indent}<Value xsi:type="v8:FixedArray">') + for v in value: + norm = normalize_choice_value(v) + lines.append(f'{indent}\t<v8:Value xsi:type="FormChoiceListDesTimeValue">') + lines.append(f'{indent}\t\t<Presentation/>') + lines.append(f'{indent}\t\t{choice_value_tag(norm)}') + lines.append(f'{indent}\t</v8:Value>') + lines.append(f'{indent}</Value>') + else: + norm = normalize_choice_value(value) + lines.append(f'{indent}{choice_value_tag(norm)}') + + +def emit_choice_parameters(lines, el, indent): + # <ChoiceParameters> (параметры выбора поля ввода) — [{name, value}]. value через + # normalize_choice_value; массив значений → FixedArray. Рус. синонимы имя/значение. + cp = el.get('choiceParameters') or [] + if not cp: + return + lines.append(f'{indent}<ChoiceParameters>') + for item in cp: + if isinstance(item, str): + item = from_choice_param_shorthand(item) + name = get_el_prop(item, ('name', 'имя')) + has_val = isinstance(item, dict) and ('value' in item or 'значение' in item) + val = get_el_prop(item, ('value', 'значение')) + name_s = '' if name is None else str(name) + lines.append(f'{indent}\t<app:item name="{esc_xml(name_s)}">') + # Параметр выбора без значения → <app:value xsi:nil="true"/> (платформа, 13 в корпусе); + # со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue. + if not has_val: + lines.append(f'{indent}\t\t<app:value xsi:nil="true"/>') + else: + lines.append(f'{indent}\t\t<app:value xsi:type="FormChoiceListDesTimeValue">') + emit_choice_param_value(lines, val, f'{indent}\t\t\t') + lines.append(f'{indent}\t\t</app:value>') + lines.append(f'{indent}\t</app:item>') + lines.append(f'{indent}</ChoiceParameters>') + + +def emit_choice_parameter_links(lines, el, indent): + # <ChoiceParameterLinks> (связи параметров выбора) — [{name, dataPath, valueChange?}]. + # valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. + cpl = el.get('choiceParameterLinks') or [] + if not cpl: + return + lines.append(f'{indent}<ChoiceParameterLinks>') + for lk in cpl: + if isinstance(lk, str): + lk = from_choice_param_link_shorthand(lk) + name = get_el_prop(lk, ('name', 'имя')) + dp = get_el_prop(lk, ('dataPath', 'path', 'путь')) + vc_raw = get_el_prop(lk, ('valueChange', 'режимИзменения')) + vc = 'Clear' + if vc_raw: + s = str(vc_raw).lower() + if s in ('clear', 'очистить', 'очистка'): + vc = 'Clear' + elif s in ('dontchange', 'неизменять', 'неменять', 'нет'): + vc = 'DontChange' + else: + vc = str(vc_raw) + name_s = '' if name is None else str(name) + dp_s = '' if dp is None else str(dp) + lines.append(f'{indent}\t<xr:Link>') + lines.append(f'{indent}\t\t<xr:Name>{esc_xml(name_s)}</xr:Name>') + lines.append(f'{indent}\t\t<xr:DataPath xsi:type="xs:string">{esc_xml(dp_s)}</xr:DataPath>') + lines.append(f'{indent}\t\t<xr:ValueChange>{vc}</xr:ValueChange>') + lines.append(f'{indent}\t</xr:Link>') + lines.append(f'{indent}</ChoiceParameterLinks>') + + +def emit_type_link(lines, el, indent): + # <TypeLink> (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. + tl = el.get('typeLink') + if not tl: + return + if isinstance(tl, str): + tl = from_type_link_shorthand(tl) + dp = get_el_prop(tl, ('dataPath', 'path', 'путь')) + li = get_el_prop(tl, ('linkItem', 'элементСвязи')) + if li is None: + li = 0 + dp_s = '' if dp is None else str(dp) + lines.append(f'{indent}<TypeLink>') + lines.append(f'{indent}\t<xr:DataPath>{esc_xml(dp_s)}</xr:DataPath>') + lines.append(f'{indent}\t<xr:LinkItem>{li}</xr:LinkItem>') + lines.append(f'{indent}</TypeLink>') + + +def normalize_radio_button_type(raw): + if not raw: + return "Auto" + s = str(raw).strip().lower() + if s in ("auto", "авто"): + return "Auto" + if s in ("radiobutton", "radiobuttons", "переключатель", "радио"): + return "RadioButtons" + if s in ("tumbler", "тумблер"): + return "Tumbler" + return str(raw).strip() + + +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, '')) + + +# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. +# Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции). +# Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён). +def get_event_pairs(el, element_name): + pairs = [] + events = el.get('events') + if events: + for ev_name, val in events.items(): + handler = '' if val is None else str(val) + if not handler: + handler = get_handler_name(element_name, ev_name) + pairs.append((ev_name, handler)) + elif el.get('on'): + handlers = el.get('handlers') or {} + for evt in el['on']: + evt_name = str(evt) + if handlers.get(evt_name): + handler = str(handlers[evt_name]) + else: + handler = get_handler_name(element_name, evt_name) + pairs.append((evt_name, handler)) + return pairs + + +# Проверить, подключено ли событие к элементу (в любом из форматов). +def test_element_event(el, event_name): + events = el.get('events') + if events and event_name in events: + return True + return event_name in (el.get('on') or []) + + +def emit_events(lines, el, element_name, indent, type_key): + pairs = get_event_pairs(el, element_name) + if not pairs: + return + + # Validate event names + if type_key and type_key in KNOWN_EVENTS: + allowed = KNOWN_EVENTS[type_key] + for ev_name, _ in pairs: + if allowed and str(ev_name) not in allowed: + print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") + + lines.append(f"{indent}<Events>") + for ev_name, handler in pairs: + lines.append(f'{indent}\t<Event name="{ev_name}">{handler}</Event>') + lines.append(f"{indent}</Events>") + + +# Детектор «настоящей» inline-разметки (1С: <link>/<b>/<color>/… и </>). Должен быть +# идентичен form-decompile/form-compile.ps1, иначе гибрид-раундтрип поедет. +_FMT_MARKUP_RE = re.compile(r'</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)', re.I) + + +def _has_real_markup(text): + if text is None: + return False + vals = list(text.values()) if isinstance(text, dict) else [text] + return any(_FMT_MARKUP_RE.search(str(v)) for v in vals) + + +def resolve_ml_formatted(val): + # {text, formatted} = явный override; строка/мапа → авто-детект formatted + if isinstance(val, dict) and 'text' in val: + return val['text'], bool(val.get('formatted')) + return val, _has_real_markup(val) + + +# ExtendedTooltip — это LabelDecoration: own-content (layout/оформление/флаги/hyperlink) ±текст. +# Признак структурированной формы: объект с любым НЕ-текстовым ключом ({text,formatted}/{ru,en} → текст). +COMPANION_STRUCT_KEYS = { + 'width', 'autoMaxWidth', 'maxWidth', 'height', 'autoMaxHeight', 'maxHeight', 'verticalAlign', 'titleHeight', + 'horizontalStretch', 'verticalStretch', 'horizontalAlign', 'groupHorizontalAlign', 'groupVerticalAlign', + 'visible', 'hidden', 'enabled', 'disabled', 'hyperlink', 'events', + 'textColor', 'backColor', 'borderColor', 'font', 'border', 'цветтекста', 'цветфона', 'цветрамки', 'шрифт', 'рамка', +} + + +def emit_companion_title(lines, content, indent): + text, fmt = resolve_ml_formatted(content) + lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') + emit_ml_items(lines, f'{indent}\t', text) + lines.append(f'{indent}') + + +def emit_companion(lines, tag, name, indent, content=None): + cid = new_id() + has_content = content is not None and not (isinstance(content, str) and content == '') + if not has_content: + lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') + return + inner = f'{indent}\t' + lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') + if isinstance(content, dict) and any(k in content for k in COMPANION_STRUCT_KEYS): + # own-content ПЕРЕД Title (в корпусе layout-first 582 vs 10). + emit_common_flags(lines, content, inner) + if content.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, content, inner) + emit_appearance(lines, content, inner, 'decoration') + if 'text' in content: + emit_companion_title(lines, content, inner) + # События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки) + emit_events(lines, content, name, inner, 'label') + else: + emit_companion_title(lines, content, inner) + lines.append(f'{indent}') + + +def emit_companion_panel(lines, tag, name, indent, panel): + # Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, horizontalAlign?, children?[] } + # или массив = shorthand для { children }. Пусто/нет → self-closing. + cid = new_id() + autofill = None + halign = None + children = None + if isinstance(panel, list): + children = panel + elif panel is not None: + if panel.get('autofill') is not None: + autofill = bool(panel.get('autofill')) + if panel.get('horizontalAlign'): + halign = str(panel.get('horizontalAlign')) + children = panel.get('children') + has_children = bool(children) and len(children) > 0 + # Платформа пишет только при false; true = дефолт (тег опускается). + emit_af_false = (autofill is False) + if not emit_af_false and not has_children and not halign: + lines.append(f'{indent}<{tag} name="{name}" id="{cid}"/>') + return + lines.append(f'{indent}<{tag} name="{name}" id="{cid}">') + if halign: + lines.append(f'{indent}\t{halign}') + if emit_af_false: + lines.append(f'{indent}\tfalse') + if has_children: + lines.append(f'{indent}\t') + for c in children: + emit_element(lines, c, f'{indent}\t\t', in_cmd_bar=True) + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type + суффикс имени. +ADDITION_TYPE_MAP = { + 'searchString': {'tag': 'SearchStringAddition', 'type': 'SearchStringRepresentation', 'suffix': 'СтрокаПоиска'}, + 'viewStatus': {'tag': 'ViewStatusAddition', 'type': 'ViewStatusRepresentation', 'suffix': 'СостояниеПросмотра'}, + 'searchControl': {'tag': 'SearchControlAddition', 'type': 'SearchControl', 'suffix': 'УправлениеПоиском'}, +} +ADDITION_KEY_SYNONYMS = { + 'searchString': ['SearchStringAddition', 'SearchStringRepresentation', 'строкаПоиска', 'отображениеСтрокиПоиска'], + 'viewStatus': ['ViewStatusAddition', 'ViewStatusRepresentation', 'состояниеПросмотра'], + 'searchControl': ['SearchControlAddition', 'SearchControl', 'управлениеПоиском'], +} +# Имя текущей таблицы — дефолт source для кастомных дополнений в commandBar. +_current_table_name = {'name': None} + + +def get_hlocation(el): + # HorizontalLocation: auto (дефолт, опускаем) / left / right; forgiving + рус. + if not isinstance(el, dict): + return None + v = el.get('horizontalLocation') + if not v: + return None + s = str(v).lower() + if s in ('auto', 'авто'): + return None + if s in ('left', 'слева', 'лево'): + return 'Left' + if s in ('right', 'справа', 'право'): + return 'Right' + if s in ('center', 'центр', 'по центру'): + return 'Center' + return str(v) + + +def emit_addition_body(lines, props, source, src_type, add_name, indent): + # Тело дополнения: AdditionSource + свойства (как у поля) + companions. props может быть None. + inner = f'{indent}\t' + lines.append(f'{inner}') + lines.append(f'{inner}\t{source}') + lines.append(f'{inner}\t{src_type}') + lines.append(f'{inner}') + if props: + if props.get('title'): + emit_mltext(lines, inner, 'Title', props['title']) + emit_common_flags(lines, props, inner) + if props.get('tooltip'): + emit_mltext(lines, inner, 'ToolTip', props['tooltip']) + if props.get('tooltipRepresentation'): + lines.append(f'{inner}{props["tooltipRepresentation"]}') + hl = get_hlocation(props) + if hl: + lines.append(f'{inner}{hl}') + emit_layout(lines, props, inner) + emit_appearance(lines, props, inner, 'field') + emit_companion(lines, 'ContextMenu', f'{add_name}КонтекстноеМеню', inner) + emit_companion(lines, 'ExtendedTooltip', f'{add_name}РасширеннаяПодсказка', inner) + + +def emit_addition(lines, el, name, eid, type_key, indent): + # Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу. + m = ADDITION_TYPE_MAP[type_key] + source = el.get('source') or _current_table_name['name'] or '' + lines.append(f'{indent}<{m["tag"]} name="{name}" id="{eid}"{di_attr(el)}>') + emit_addition_body(lines, el, source, m['type'], name, indent) + lines.append(f'{indent}') + + +def emit_table_addition(lines, type_key, table_name, indent, override=None): + # Стандартное табличное дополнение (авто-генерация). override — объект отклонений из карты additions. + m = ADDITION_TYPE_MAP[type_key] + add_name = f'{table_name}{m["suffix"]}' + aid = new_id() + lines.append(f'{indent}<{m["tag"]} name="{add_name}" id="{aid}">') + emit_addition_body(lines, override, table_name, m['type'], add_name, indent) + lines.append(f'{indent}') + + +def get_addition_override(additions, type_key): + # Прочитать override-объект для типа из per-table карты additions (с синонимами). + if not isinstance(additions, dict): + return None + for k in [type_key] + ADDITION_KEY_SYNONYMS[type_key]: + if k in additions: + return additions[k] + return None + + +# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). +# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). +# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. +# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. +def emit_xr_flag(lines, tag, val, indent): + if val is None: + return + if isinstance(val, bool): + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t{'true' if val else 'false'}") + lines.append(f"{indent}") + return + # объектная форма { common, roles } + common = bool(val.get('common')) if val.get('common') is not None else False + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t{'true' if common else 'false'}") + roles = val.get('roles') + if roles: + for rname, rval in roles.items(): + # Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.". + # Роль по GUID (заимствованная/расширение — name="" без префикса) эмитим как есть. + rn = re.sub(r'^(Role|Роль)\.', '', rname) + if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', rn): + rn = "Role." + rn + lines.append(f"{indent}\t{'true' if rval else 'false'}") + lines.append(f"{indent}") + + +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('userVisible') is not None: + emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent) + 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") + + +# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag. +def emit_common_element_props(lines, el, indent): + if el.get('defaultItem') is True: + lines.append(f"{indent}true") + if 'skipOnInput' in el and el['skipOnInput'] is not None: + siv = 'true' if el['skipOnInput'] is True else 'false' + lines.append(f"{indent}{siv}") + # EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) + if el.get('enableStartDrag') is not None: + lines.append(f'{indent}{"true" if el["enableStartDrag"] else "false"}') + if el.get('fileDragMode'): + lines.append(f"{indent}{el['fileDragMode']}") + # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» + for key, tag in (('showInHeader', 'ShowInHeader'), ('showInFooter', 'ShowInFooter'), ('autoCellHeight', 'AutoCellHeight')): + if el.get(key) is not None: + lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') + if el.get('footerHorizontalAlign'): + lines.append(f"{indent}{el['footerHorizontalAlign']}") + if el.get('headerHorizontalAlign'): + lines.append(f"{indent}{el['headerHorizontalAlign']}") + + +def emit_picture_ref(lines, val, pic_tag, indent): + """Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). + Платформа ВСЕГДА эмитит → пишем всегда (false по умолчанию). + Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}. + src с префиксом "abs:" → встроенная картинка ; иначе .""" + if not val: + return + tpx = None + if isinstance(val, str): + src, lt = val, False + else: + src = val.get('src') + lt = val.get('loadTransparent') is True + tpx = val.get('transparentPixel') + if not src: + return + src_str = str(src) + lines.append(f"{indent}<{pic_tag}>") + if src_str.startswith('abs:'): + lines.append(f"{indent}\t{esc_xml(src_str[4:])}") + else: + lines.append(f"{indent}\t{esc_xml(src_str)}") + lines.append(f'{indent}\t{"true" if lt else "false"}') + if tpx: + lines.append(f'{indent}\t') + lines.append(f"{indent}") + + +def emit_column_pics(lines, el, indent): + """Картинки заголовка/подвала колонки поля — по схеме сразу после , + перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь).""" + emit_picture_ref(lines, el.get('headerPicture'), 'HeaderPicture', indent) + emit_picture_ref(lines, el.get('footerPicture'), 'FooterPicture', indent) + + +def emit_command_picture(lines, pic, elem_lt, indent): + """ кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false + (обратная конвенция относительно header/values-картинок). Прощающий ввод: + принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель + опишет картинку объектно по аналогии с headerPicture. elem_lt — legacy + элемент-уровневый ключ loadTransparent (если в объекте флаг не задан).""" + if not pic: + return + lt = None + tpx = None + if isinstance(pic, str): + src = pic + else: + src = pic.get('src') + if pic.get('loadTransparent') is not None: + lt = bool(pic.get('loadTransparent')) + tpx = pic.get('transparentPixel') + if not src: + return + if lt is None and elem_lt is not None: + lt = bool(elem_lt) + src_str = str(src) + lines.append(f'{indent}') + if src_str.startswith('abs:'): + lines.append(f'{indent}\t{esc_xml(src_str[4:])}') + else: + lines.append(f'{indent}\t{esc_xml(src_str)}') + lines.append(f'{indent}\t{"false" if lt is False else "true"}') + if tpx: + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# --- Оформление элемента: цвета / шрифты / граница (зеркало form-compile.ps1 Emit-Appearance) --- +# Прямые свойства элемента (// + header/footer у полей). Ключи англ. +# camelCase 1:1 с тегами + приём рус. синонимов. Цвет — verbatim-строка (style:/web:/win:/#RRGGBB); +# шрифт — строка-ref/объект-атрибуты; граница — строка-ref/ +# объект {width,style}. Порядок тегов — XSD (профиль по базовому типу). +APPEARANCE_SPEC = { + 'titleTextColor': ('TitleTextColor', 'color'), + 'titleBackColor': ('TitleBackColor', 'color'), + 'titleFont': ('TitleFont', 'font'), + 'footerTextColor': ('FooterTextColor', 'color'), + 'footerBackColor': ('FooterBackColor', 'color'), + 'footerFont': ('FooterFont', 'font'), + 'textColor': ('TextColor', 'color'), + 'backColor': ('BackColor', 'color'), + 'borderColor': ('BorderColor', 'color'), + 'border': ('Border', 'border'), + 'font': ('Font', 'font'), +} +APPEARANCE_SYNONYMS = { + 'цветтекста': 'textColor', 'цветфона': 'backColor', 'цветрамки': 'borderColor', + 'цветтекстазаголовка': 'titleTextColor', 'цветфоназаголовка': 'titleBackColor', 'шрифтзаголовка': 'titleFont', + 'цветтекстаподвала': 'footerTextColor', 'цветфонаподвала': 'footerBackColor', 'шрифтподвала': 'footerFont', + 'шрифт': 'font', 'рамка': 'border', +} +# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ. +# Ключи нормализованы (lowercase, без пробелов); сопоставление в emit_element тоже. Англ. ключ +# работает всегда (доп. слой прощающего ввода). Видимость/Доступность НЕ включаем (hidden/disabled инвертирован). +PROP_SYNONYMS = { + 'пометка': 'checked', + 'кнопкавыбора': 'choiceButton', 'кнопкаочистки': 'clearButton', 'кнопкарегулирования': 'spinButton', + 'кнопкавыпадающегосписка': 'dropListButton', 'кнопкасписковоговыбора': 'choiceListButton', + 'кнопкаоткрытия': 'openButton', 'кнопкапоумолчанию': 'defaultButton', + 'быстрыйвыбор': 'quickChoice', 'формавыбора': 'choiceForm', 'историявыборапривводе': 'choiceHistoryOnInput', + 'выборгруппиэлементов': 'choiceFoldersAndItems', 'фиксациявтаблице': 'fixingInTable', + 'путькданнымподвала': 'footerDataPath', 'автоотметканезаполненного': 'markIncomplete', + 'многострочныйрежим': 'multiLine', 'режимпароля': 'passwordMode', 'переноспословам': 'wrap', + 'расположениезаголовка': 'titleLocation', 'пропускатьпривводе': 'skipOnInput', + 'заголовок': 'title', 'ширина': 'width', 'высота': 'height', 'подсказкаввода': 'inputHint', +} +APP_ORDER_FIELD =['titleTextColor', 'titleBackColor', 'titleFont', 'footerTextColor', 'footerBackColor', 'footerFont', 'textColor', 'backColor', 'borderColor', 'border', 'font'] +APP_ORDER_DECORATION = ['textColor', 'font', 'backColor', 'borderColor', 'border'] +APP_ORDER_BUTTON = ['textColor', 'backColor', 'borderColor', 'font'] + + +def get_appearance_value(el, canonical): + if not isinstance(el, dict): + return None + if canonical in el: + return el[canonical] + lowmap = {k.lower(): k for k in el.keys()} + if canonical.lower() in lowmap: + return el[lowmap[canonical.lower()]] + for syn, canon in APPEARANCE_SYNONYMS.items(): + if canon == canonical and syn in lowmap: + return el[lowmap[syn]] + return None + + +def emit_font_tag(lines, tag, val, indent): + if isinstance(val, str): + lines.append(f'{indent}<{tag} ref="{esc_xml(val)}" kind="StyleItem"/>') + return + attrs = [] + for a in ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale'): + if a in val and val[a] is not None: + v = val[a] + if isinstance(v, bool): + v = 'true' if v else 'false' + attrs.append(f'{a}="{esc_xml(str(v))}"') + lines.append(f'{indent}<{tag} {" ".join(attrs)}/>') + + +def emit_border_tag(lines, val, indent): + if isinstance(val, str): + lines.append(f'{indent}') + return + if val.get('ref'): + lines.append(f'{indent}') + return + width = val['width'] if val.get('width') is not None else 1 + style = str(val['style']) if 'style' in val else None + lines.append(f'{indent}') + if style: + lines.append(f'{indent}\t{esc_xml(style)}') + lines.append(f'{indent}') + + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time — зеркало Emit-PlannerSettings (ps1). +PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner' +CHART_NS = 'http://v8.1c.ru/8.2/data/chart' + + +def _pl_get(o, k, default=None): + if isinstance(o, dict) and o.get(k) is not None: + return o[k] + return default + + +def _pl_bool(v): + if isinstance(v, bool): + return 'true' if v else 'false' + if str(v) == 'True': + return 'true' + if str(v) == 'False': + return 'false' + return str(v) + + +def emit_planner_color(lines, tag, o, key, ind): + lines.append(f'{ind}{esc_xml(str(_pl_get(o, key, "auto")))}') + + +def emit_planner_text(lines, tag, v, ind): + if v is None or str(v) == '': + lines.append(f'{ind}') + else: + lines.append(f'{ind}{esc_xml(str(v))}') + + +_PLANNER_REF_RE = re.compile( + r'^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' + r'|\.EnumValue\.|EmptyRef$' + r'|^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.') + + +def test_planner_ref(v): + return bool(_PLANNER_REF_RE.search(str(v))) + + +def emit_planner_value(lines, v, ind): + if v is None or str(v) == '': + lines.append(f'{ind}') + return + t = 'xr:DesignTimeRef' if test_planner_ref(v) else 'xs:string' + lines.append(f'{ind}{esc_xml(str(v))}') + + +def emit_planner_font(lines, o, ind): + f = _pl_get(o, 'font') + if f is None: + lines.append(f'{ind}') + return + emit_font_tag(lines, 'pl:font', f, ind) + + +def emit_planner_border(lines, o, ind, key='border'): + b = _pl_get(o, key) + bw = _pl_get(b, 'width', 1) if b else 1 + bs = _pl_get(b, 'style', 'Single') if b else 'Single' + lines.append(f'{ind}') + lines.append(f'{ind}\t{esc_xml(str(bs))}') + lines.append(f'{ind}') + + +def emit_planner_level(lines, lv, cns, ind): + li = f'{ind}\t' + lines.append(f'{ind}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "measure", "Hour")))}') + lines.append(f'{li}{_pl_get(lv, "interval", 1)}') + lines.append(f'{li}{_pl_bool(_pl_get(lv, "show", True))}') + line = _pl_get(lv, 'line') + lw = _pl_get(line, 'width', 1) if line else 1 + lg = _pl_get(line, 'gap', False) if line else False + lst = _pl_get(line, 'style', 'Solid') if line else 'Solid' + lines.append(f'{li}') + lines.append(f'{li}\t{esc_xml(str(lst))}') + lines.append(f'{li}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "scaleColor", "auto")))}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "dayFormatRule", "MonthDayWeekDay")))}') + fmt = _pl_get(lv, 'format') + if fmt is None: + fmt = {'#': 'DF="HH:mm"', 'ru': 'DF="HH:mm"'} + lines.append(f'{li}') + emit_ml_items(lines, f'{li}\t', fmt) + lines.append(f'{li}') + labels = _pl_get(lv, 'labels') + ticks = _pl_get(labels, 'ticks', 0) if labels else 0 + lines.append(f'{li}') + lines.append(f'{li}\t{ticks}') + lines.append(f'{li}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "backColor", "auto")))}') + lines.append(f'{li}{esc_xml(str(_pl_get(lv, "textColor", "auto")))}') + lines.append(f'{li}{_pl_bool(_pl_get(lv, "showPereodicalLabels", True))}') + lines.append(f'{ind}') + + +def emit_planner_timescale(lines, ts, ind): + cns = CHART_NS + ci = f'{ind}\t' + lines.append(f'{ind}') + placement = _pl_get(ts, 'placement', 'Left') if ts else 'Left' + lines.append(f'{ci}{esc_xml(str(placement))}') + levels = _pl_get(ts, 'levels', []) if ts else [] + if not levels: + levels = [None] + for lv in levels: + emit_planner_level(lines, lv, cns, ci) + transp = _pl_get(ts, 'transparent', False) if ts else False + lines.append(f'{ci}{_pl_bool(transp)}') + tbc = _pl_get(ts, 'backColor', 'auto') if ts else 'auto' + ttc = _pl_get(ts, 'textColor', 'auto') if ts else 'auto' + tcl = _pl_get(ts, 'currentLevel', 0) if ts else 0 + lines.append(f'{ci}{esc_xml(str(tbc))}') + lines.append(f'{ci}{esc_xml(str(ttc))}') + lines.append(f'{ci}{tcl}') + lines.append(f'{ind}') + + +def emit_planner_item(lines, it, ind): + lines.append(f'{ind}') + ii = f'{ind}\t' + emit_planner_value(lines, _pl_get(it, 'value'), ii) + emit_planner_text(lines, 'text', _pl_get(it, 'text', ''), ii) + emit_planner_text(lines, 'tooltip', _pl_get(it, 'tooltip', ''), ii) + lines.append(f'{ii}{_pl_get(it, "begin", "0001-01-01T00:00:00")}') + lines.append(f'{ii}{_pl_get(it, "end", "0001-01-01T00:00:00")}') + emit_planner_color(lines, 'borderColor', it, 'borderColor', ii) + emit_planner_color(lines, 'backColor', it, 'backColor', ii) + emit_planner_color(lines, 'textColor', it, 'textColor', ii) + emit_planner_font(lines, it, ii) + lines.append(f'{ii}') + lines.append(f'{ii}{_pl_get(it, "replacementDate", "0001-01-01T00:00:00")}') + lines.append(f'{ii}{_pl_bool(_pl_get(it, "deleted", False))}') + iid = _pl_get(it, 'id') + if iid is None: + import uuid + iid = str(uuid.uuid4()) + lines.append(f'{ii}{iid}') + lines.append(f'{ii}{_pl_bool(_pl_get(it, "textFormatted", False))}') + emit_planner_border(lines, it, ii, 'border') + lines.append(f'{ii}{esc_xml(str(_pl_get(it, "editMode", "EnableEdit")))}') + lines.append(f'{ind}') + + +def emit_planner_dim_element(lines, el, ind): + lines.append(f'{ind}') + ii = f'{ind}\t' + emit_planner_value(lines, _pl_get(el, 'value'), ii) + emit_planner_text(lines, 'text', _pl_get(el, 'text', ''), ii) + emit_planner_color(lines, 'borderColor', el, 'borderColor', ii) + emit_planner_color(lines, 'backColor', el, 'backColor', ii) + emit_planner_color(lines, 'textColor', el, 'textColor', ii) + emit_planner_font(lines, el, ii) + for sub in _pl_get(el, 'elements', []): + emit_planner_dim_element(lines, sub, ii) + lines.append(f'{ii}{_pl_bool(_pl_get(el, "showOnlySubordinatesAreas", True))}') + lines.append(f'{ii}{_pl_bool(_pl_get(el, "textFormatted", False))}') + lines.append(f'{ind}') + + +def emit_planner_dimension(lines, d, ind): + lines.append(f'{ind}') + di = f'{ind}\t' + emit_planner_value(lines, _pl_get(d, 'value'), di) + emit_planner_text(lines, 'text', _pl_get(d, 'text', ''), di) + emit_planner_color(lines, 'borderColor', d, 'borderColor', di) + emit_planner_color(lines, 'backColor', d, 'backColor', di) + emit_planner_color(lines, 'textColor', d, 'textColor', di) + emit_planner_font(lines, d, di) + for el in _pl_get(d, 'elements', []): + emit_planner_dim_element(lines, el, di) + lines.append(f'{di}{_pl_bool(_pl_get(d, "textFormatted", False))}') + lines.append(f'{ind}') + + +def emit_planner_settings(lines, pl, ind): + lines.append(f'{ind}') + si = f'{ind}\t' + for it in _pl_get(pl, 'items', []): + emit_planner_item(lines, it, si) + for d in _pl_get(pl, 'dimensions', []): + emit_planner_dimension(lines, d, si) + emit_planner_color(lines, 'borderColor', pl, 'borderColor', si) + emit_planner_color(lines, 'backColor', pl, 'backColor', si) + emit_planner_color(lines, 'textColor', pl, 'textColor', si) + emit_planner_color(lines, 'lineColor', pl, 'lineColor', si) + emit_planner_font(lines, pl, si) + lines.append(f'{si}{_pl_get(pl, "beginOfRepresentationPeriod", "0001-01-01T00:00:00")}') + lines.append(f'{si}{_pl_get(pl, "endOfRepresentationPeriod", "0001-01-01T00:00:00")}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "alignElementsOfTimeScale", True))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayTimeScaleWrapHeaders", True))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayWrapHeaders", True))}') + wfmt = _pl_get(pl, 'timeScaleWrapHeadersFormat') + if wfmt is None: + wfmt = {'#': 'DLF="DD"', 'ru': 'DLF="DD"'} + emit_mltext(lines, si, 'pl:timeScaleWrapHeadersFormat', wfmt) + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "periodicVariantUnit", "Day")))}') + lines.append(f'{si}{_pl_get(pl, "periodicVariantRepetition", 1)}') + lines.append(f'{si}{_pl_get(pl, "timeScaleWrapBeginIndent", 0)}') + lines.append(f'{si}{_pl_get(pl, "timeScaleWrapEndIndent", 0)}') + emit_planner_timescale(lines, _pl_get(pl, 'timeScale'), si) + period = _pl_get(pl, 'period') + if period: + lines.append(f'{si}') + lines.append(f'{si}\t{_pl_get(period, "begin", "0001-01-01T00:00:00")}') + lines.append(f'{si}\t{_pl_get(period, "end", "0001-01-01T00:00:00")}') + lines.append(f'{si}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "displayCurrentDate", True))}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsTimeRepresentation", "BeginTime")))}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "itemsBehaviorWhenSpaceInsufficient", "CollapseItems")))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinColumnWidth", True))}') + lines.append(f'{si}{_pl_bool(_pl_get(pl, "autoMinRowHeight", True))}') + lines.append(f'{si}{_pl_get(pl, "minColumnWidth", 0)}') + lines.append(f'{si}{_pl_get(pl, "minRowHeight", 0)}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixDimensionsHeader", "auto")))}') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "fixTimeScaleHeader", "auto")))}') + emit_planner_border(lines, pl, si, 'border') + lines.append(f'{si}{esc_xml(str(_pl_get(pl, "newItemsTextType", "String")))}') + lines.append(f'{ind}') + + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time — генерик-эмиттер (зеркало +# Build-ChartNode декомпилятора + Emit-ChartNode ps1). +CHART_ML_FIELDS = {'title', 'lbFormat', 'lbpFormat', 'vsFormat', 'dtFormat', 'dataSourceDescription', 'labelFormat', 'text'} +CHART_ATTR_FIELDS = {'gaugeQualityBands'} +CHART_FONT_KEYS = ('ref', 'faceName', 'height', 'bold', 'italic', 'underline', 'strikeout', 'kind', 'scale') + + +def emit_chart_node(lines, name, val, ind): + if name in CHART_ML_FIELDS: + if val is None or str(val) == '': + lines.append(f'{ind}') + return + lines.append(f'{ind}') + emit_ml_items(lines, f'{ind}\t', val) + lines.append(f'{ind}') + return + if isinstance(val, list): + for e in val: + emit_chart_node(lines, name, e, ind) + return + if isinstance(val, dict): + keys = list(val.keys()) + if name in CHART_ATTR_FIELDS: + attrs = ' '.join(f'{k}="{esc_xml(_pl_bool(val[k]) if isinstance(val[k], bool) else str(val[k]))}"' for k in keys) + lines.append(f'{ind}') + return + if 'gap' in val: + lines.append(f'{ind}') + lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') + lines.append(f'{ind}') + return + if 'style' in val and 'width' in val: + lines.append(f'{ind}') + lines.append(f'{ind}\t{esc_xml(str(val.get("style")))}') + lines.append(f'{ind}') + return + if any(fk in val for fk in CHART_FONT_KEYS): + attrs = ' '.join(f'{fk}="{esc_xml(_pl_bool(val[fk]) if isinstance(val[fk], bool) else str(val[fk]))}"' for fk in CHART_FONT_KEYS if fk in val) + lines.append(f'{ind}') + return + if not keys: + lines.append(f'{ind}') + return + lines.append(f'{ind}') + for k in keys: + emit_chart_node(lines, k, val[k], f'{ind}\t') + lines.append(f'{ind}') + return + if val is None or str(val) == '': + lines.append(f'{ind}') + return + if isinstance(val, bool): + lines.append(f'{ind}{_pl_bool(val)}') + return + lines.append(f'{ind}{esc_xml(str(val))}') + + +def emit_chart_settings(lines, chart, ind, ctype='d4p1:Chart'): + lines.append(f'{ind}') + for k in list(chart.keys()): + emit_chart_node(lines, k, chart[k], f'{ind}\t') + lines.append(f'{ind}') + + +def emit_appearance(lines, el, indent, profile='field'): + if not isinstance(el, dict): + return + order = {'decoration': APP_ORDER_DECORATION, 'button': APP_ORDER_BUTTON}.get(profile, APP_ORDER_FIELD) + for key in order: + val = get_appearance_value(el, key) + if val is None or (isinstance(val, str) and val == ''): + continue + tag, kind = APPEARANCE_SPEC[key] + if kind == 'color': + lines.append(f'{indent}<{tag}>{esc_xml(str(val))}') + elif kind == 'font': + emit_font_tag(lines, tag, val, indent) + else: + emit_border_tag(lines, val, indent) + + +# Простые скаляры элемента (pass-through, зеркало $script:genericScalars). kind bool/value. +GENERIC_SCALARS = [ + ('VerticalAlign', 'verticalAlign', 'value'), + ('ThroughAlign', 'throughAlign', 'value'), + ('EnableContentChange', 'enableContentChange', 'bool'), + ('PictureSize', 'pictureSize', 'value'), + ('TitleHeight', 'titleHeight', 'value'), + ('ChildItemsWidth', 'childItemsWidth', 'value'), + ('ShowLeftMargin', 'showLeftMargin', 'bool'), + ('CellHyperlink', 'cellHyperlink', 'bool'), + ('ViewMode', 'viewMode', 'value'), + ('VerticalScrollBar', 'verticalScrollBar', 'value'), + ('RowInputMode', 'rowInputMode', 'value'), + ('Mask', 'mask', 'value'), + ('CreateButton', 'createButton', 'bool'), + ('FixingInTable', 'fixingInTable', 'value'), + ('VerticalSpacing', 'verticalSpacing', 'value'), + # Spec-fields (document/gauge) - type-specific enum/bool scalars pass-through + ('HorizontalScrollBar', 'horizontalScrollBar', 'value'), + ('ViewScalingMode', 'viewScalingMode', 'value'), + ('Output', 'output', 'value'), + ('SelectionShowMode', 'selectionShowMode', 'value'), + ('PointerType', 'pointerType', 'value'), + ('DrawingSelectionShowMode', 'drawingSelectionShowMode', 'value'), + ('WarningOnEditRepresentation', 'warningOnEditRepresentation', 'value'), + ('MarkingAppearance', 'markingAppearance', 'value'), + ('Protection', 'protection', 'bool'), + ('Edit', 'edit', 'bool'), + ('ShowGrid', 'showGrid', 'bool'), + ('ShowGroups', 'showGroups', 'bool'), + ('ShowHeaders', 'showHeaders', 'bool'), + ('ShowRowAndColumnNames', 'showRowAndColumnNames', 'bool'), + ('ShowCellNames', 'showCellNames', 'bool'), + ('ShowPercent', 'showPercent', 'bool'), + # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы + ('HorizontalSpacing', 'horizontalSpacing', 'value'), + ('RepresentationInContextMenu', 'representationInContextMenu', 'value'), + ('SettingsNamedItemDetailedRepresentation', 'settingsNamedItemDetailedRepresentation', 'bool'), + # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) + ('ItemHeight', 'itemHeight', 'value'), + ('DropListWidth', 'dropListWidth', 'value'), + # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам + ('TitleDataPath', 'titleDataPath', 'value'), + ('ExtendedEdit', 'extendedEdit', 'bool'), + ('MaxRowsCount', 'maxRowsCount', 'value'), + ('AutoMaxRowsCount', 'autoMaxRowsCount', 'bool'), + ('HeightControlVariant', 'heightControlVariant', 'value'), + ('EditTextUpdate', 'editTextUpdate', 'value'), + # Корпусный хвост: свёртка группы / форма попапа / авто-добавление / выделение отрицательных / + # нач. позиция списка / высота списка выбора / три состояния / прокрутка страницы при сжатии + ('ControlRepresentation', 'controlRepresentation', 'value'), + ('ShapeRepresentation', 'shapeRepresentation', 'value'), + ('AutoAddIncomplete', 'autoAddIncomplete', 'bool'), + ('MarkNegatives', 'markNegatives', 'bool'), + ('InitialListView', 'initialListView', 'value'), + ('ChoiceListHeight', 'choiceListHeight', 'value'), + ('ThreeState', 'threeState', 'bool'), + ('ScrollOnCompress', 'scrollOnCompress', 'bool'), + # Сочетание клавиш — общее свойство (команда — отдельный путь) + ('Shortcut', 'shortcut', 'value'), + # Батч простых скаляров (input/radio/group/picDecoration/button; Table-специфичные — отдельно) + ('IncompleteChoiceMode', 'incompleteChoiceMode', 'value'), + ('EqualColumnsWidth', 'equalColumnsWidth', 'bool'), + ('ChildrenAlign', 'childrenAlign', 'value'), + ('ImageScale', 'imageScale', 'value'), + ('Zoomable', 'zoomable', 'bool'), + ('Shape', 'shape', 'value'), + ('PictureLocation', 'pictureLocation', 'value'), + # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) + ('EqualItemsWidth', 'equalItemsWidth', 'bool'), + ('ItemTitleHeight', 'itemTitleHeight', 'value'), +] + + +def emit_generic_scalars(lines, el, indent): + for tag, key, kind in GENERIC_SCALARS: + if key not in el or el[key] is None: + continue + if kind == 'bool': + lines.append(f'{indent}<{tag}>{"true" if el[key] else "false"}') + else: + v = str(el[key]) + if v == '': + continue + lines.append(f'{indent}<{tag}>{esc_xml(v)}') + + +def emit_layout(lines, el, indent, skip_height=False, multi_line_default=False): + # Общие layout-свойства — применимы ко всем элементам. Порядок согласован + # с историческим выводом input/label, чтобы не сдвигать существующие снапшоты. + # skip_height: подавить (зарезервирован; Table теперь эмитит generic-ом + свой ). + # multi_line_default: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false. + # CommandSet (отключённые команды редактора) — общее свойство поля; в схеме рано (после TitleLocation). + if el.get('excludedCommands') and len(el['excludedCommands']) > 0: + lines.append(f'{indent}') + for cmd in el['excludedCommands']: + lines.append(f'{indent}\t{cmd}') + lines.append(f'{indent}') + emit_common_element_props(lines, el, indent) + if 'autoMaxWidth' in el: + if el.get('autoMaxWidth') is False: + lines.append(f"{indent}false") + elif multi_line_default: + lines.append(f"{indent}false") + if el.get('maxWidth') is not None: + lines.append(f"{indent}{el['maxWidth']}") + if el.get('autoMaxHeight') is False: + lines.append(f"{indent}false") + if el.get('maxHeight') is not None: + lines.append(f"{indent}{el['maxHeight']}") + if el.get('width'): + lines.append(f"{indent}{el['width']}") + if not skip_height and el.get('height'): + lines.append(f"{indent}{el['height']}") + if el.get('horizontalStretch') is not None: + lines.append(f'{indent}{"true" if el["horizontalStretch"] else "false"}') + if el.get('verticalStretch') is not None: + lines.append(f'{indent}{"true" if el["verticalStretch"] else "false"}') + if el.get('groupHorizontalAlign'): + lines.append(f"{indent}{el['groupHorizontalAlign']}") + if el.get('groupVerticalAlign'): + lines.append(f"{indent}{el['groupVerticalAlign']}") + if el.get('horizontalAlign'): + lines.append(f"{indent}{el['horizontalAlign']}") + emit_generic_scalars(lines, el, indent) + + +def title_from_name(name): + """СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'.""" + if not name: + return '' + s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name) + s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s) + parts = s.split(' ') + if not parts: + return s + out = [parts[0]] + for p in parts[1:]: + out.append(p if (len(p) > 1 and p.isupper()) else p.lower()) + return ' '.join(out) + + +def emit_title(lines, el, name, indent, auto=False): + # Нет ключа title → авто-вывод из имени (помощь модели). + # Явный title "" (или None) → подавить. Явный непустой → как есть. + if 'title' in el: + if el.get('title'): + emit_mltext(lines, indent, 'Title', el['title']) + elif auto and name: + emit_mltext(lines, indent, 'Title', title_from_name(name)) + # ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title. + if el.get('tooltip'): + emit_mltext(lines, indent, 'ToolTip', el['tooltip']) + # ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip. + if el.get('tooltipRepresentation'): + lines.append(f'{indent}{el["tooltipRepresentation"]}') + + +_TITLE_LOC_MAP = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} + + +def map_title_loc(v): + return _TITLE_LOC_MAP.get(str(v).lower(), str(v)) + + +def emit_title_location(lines, el, indent, smart_default): + # Нет ключа → умный дефолт (Right/None), эмитится. "" → подавить (дефолт платформы). + # Значение → эмитить с маппингом регистра. + if 'titleLocation' in el: + if el.get('titleLocation'): + lines.append(f"{indent}{map_title_loc(el['titleLocation'])}") + elif smart_default: + lines.append(f"{indent}{smart_default}") + + +# --- 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 object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)', + '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", + "характеристика": "Characteristic", + "любаяссылка": "AnyRef", + "любаяссылкаиб": "AnyIBRef", + # Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: + "standardperiod": "v8:StandardPeriod", + "стандартныйпериод": "v8:StandardPeriod", + "standardbeginningdate": "v8:StandardBeginningDate", + "стандартнаядатаначала": "v8:StandardBeginningDate", + "uuid": "v8:UUID", + "уникальныйидентификатор": "v8:UUID", + "списокзначений": "ValueList", +} + + +def resolve_type_str(type_str): + if not type_str: + return type_str + # Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix) + if type_str.startswith('cfg:'): + type_str = type_str[4:] + 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) + # TypeId — тип, заданный глобальным стабильным GUID (, не ). Платформа так + # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID + # глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'. + m = re.match(r'^typeid:([0-9a-fA-F-]{36})$', type_str) + if m: + lines.append(f'{indent}{m.group(1)}') + return + # boolean + if type_str == 'boolean': + lines.append(f'{indent}xs:boolean') + return + + # string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed) + m = re.match(r'^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$', type_str, re.IGNORECASE) + if m: + length = m.group(2) if m.group(2) else '0' + al = 'Fixed' if (m.group(4) and m.group(4).lower() == 'fixed') else 'Variable' + lines.append(f'{indent}xs:string') + lines.append(f'{indent}') + lines.append(f'{indent}\t{length}') + lines.append(f'{indent}\t{al}') + 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 + + # Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта. + # Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. + if type_str in ('DynamicList', 'ConstantsSet', 'ReportObject'): + lines.append(f'{indent}cfg:{type_str}') + return + + # TypeSet (набор типов) → : определяемый тип / характеристика (именованные) + # + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки. + if re.match(r'^(DefinedType|Characteristic)\.', type_str): + lines.append(f'{indent}cfg:{type_str}') + return + if re.match(r'^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$', type_str): + lines.append(f'{indent}cfg:{type_str}') + return + + # cfg: references + if CFG_REF_PATTERN.match(type_str): + lines.append(f'{indent}cfg:{type_str}') + return + + # Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на ). + # Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа. + # К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField, + # d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis. + special_type_ns = { + "mxl:SpreadsheetDocument": "http://v8.1c.ru/8.2/data/spreadsheet", + "fd:FormattedDocument": "http://v8.1c.ru/8.2/data/formatted-document", + "d5p1:TextDocument": "http://v8.1c.ru/8.1/data/txtedt", + "d5p1:Chart": "http://v8.1c.ru/8.2/data/chart", + "d5p1:GanttChart": "http://v8.1c.ru/8.2/data/chart", + "d5p1:Dendrogram": "http://v8.1c.ru/8.2/data/chart", + "d5p1:FlowchartContextType": "http://v8.1c.ru/8.2/data/graphscheme", + "d5p1:DataAnalysisTimeIntervalUnitType": "http://v8.1c.ru/8.2/data/data-analysis", + "d5p1:GeographicalSchema": "http://v8.1c.ru/8.2/data/geo", + "pdfdoc:PDFDocument": "http://v8.1c.ru/8.3/data/pdf", + "pl:Planner": "http://v8.1c.ru/8.3/data/planner", + } + if type_str in special_type_ns: + pref = type_str.split(':', 1)[0] + lines.append(f'{indent}{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]}") + # Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — verbatim (напр. v8:UUID, v8:StandardPeriod). + if re.match(r'^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):', type_str): + lines.append(f'{indent}{type_str}') + elif '.' 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, tag="Type", tag_attrs=""): + # tag/tag_attrs — обёртка (по умолчанию ); для valueType ValueList вызывается с + # tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"'. + if not type_str: + lines.append(f'{indent}<{tag}{tag_attrs}/>') + return + + type_string = str(type_str) + parts = [p.strip() for p in re.split(r'[|+]', type_string)] + + lines.append(f'{indent}<{tag}{tag_attrs}>') + for part in parts: + emit_single_type(lines, part, f'{indent}\t') + lines.append(f'{indent}') + + +# --- Element emitters --- + +def emit_element(lines, el, indent, in_cmd_bar=False): + # Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов. + normalize_panel_synonyms(el) + + # Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio). + # commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя). + for src, dst in ELEMENT_TYPE_SYNONYMS.items(): + if src in el and dst not in el: + if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): + continue + el[dst] = el.pop(src) + + # Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive. + # Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем. + for p_name in list(el.keys()): + norm = p_name.replace(' ', '').lower() + canon = PROP_SYNONYMS.get(norm) + if canon and p_name != canon: + val = el.pop(p_name) + if canon not in el: + el[canon] = val + + 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 (внутренние маркеры на _ пропускаем). Оформление (цвета/шрифты/граница) + # проверяем против самих структур appearance — канонические ключи + forgiving-синонимы, чтобы + # allowlist не дрейфовал при добавлении новых. + for p_name in el.keys(): + if p_name.startswith('_'): + continue + if p_name not in KNOWN_KEYS and p_name not in APPEARANCE_SPEC and p_name not in APPEARANCE_SYNONYMS: + 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) + _ensure_unique(name, _seen_element_names, 'element') + eid = new_id() + + emitters = { + 'group': emit_group, + 'columnGroup': emit_column_group, + 'buttonGroup': emit_button_group, + 'input': emit_input, + 'check': emit_check, + 'radio': emit_radio_button_field, + '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, + 'searchString': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchString', indent), + 'viewStatus': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'viewStatus', indent), + 'searchControl': lambda lines, el, name, eid, indent: emit_addition(lines, el, name, eid, 'searchControl', indent), + 'spreadsheet': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'SpreadSheetDocumentField', 'spreadsheet'), + 'html': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'HTMLDocumentField', 'html'), + 'textDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TextDocumentField', 'textDoc'), + 'formattedDoc': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'FormattedDocumentField', 'formattedDoc'), + 'progressBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ProgressBarField', 'progressBar'), + 'trackBar': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'TrackBarField', 'trackBar'), + 'chart': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'ChartField', 'chart'), + 'graphicalSchema': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'GraphicalSchemaField', 'graphicalSchema'), + 'planner': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PlannerField', 'planner'), + 'periodField': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'PeriodField', 'periodField'), + 'dendrogram': lambda lines, el, name, eid, indent: emit_simple_field(lines, el, name, eid, indent, 'DendrogramField', 'dendrogram'), + 'ganttChart': emit_gantt_chart, + } + + emitter = emitters.get(type_key) + if emitter: + if type_key == 'button': + emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar) + else: + 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 orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible. + group_val = str(el.get('group', '')).lower() + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'alwayshorizontal': 'AlwaysHorizontal', + 'alwaysvertical': 'AlwaysVertical', + 'horizontalifpossible': 'HorizontalIfPossible', + 'collapsible': 'Vertical', + } + orientation = orientation_map.get(group_val) + if orientation: + lines.append(f'{inner}{orientation}') + + # Behavior: ключ behavior (usual/collapsible/popup) → ; отсутствие = Авто (не эмитим). + behavior_val = str(el['behavior']).lower() if el.get('behavior') else ('collapsible' if group_val == 'collapsible' else None) + bmap = {'usual': 'Usual', 'collapsible': 'Collapsible', 'popup': 'PopUp'} + if behavior_val and behavior_val in bmap: + lines.append(f'{inner}{bmap[behavior_val]}') + # Collapsed — у Collapsible и PopUp (не привязано к одному behavior) + if el.get('collapsed') is True: + lines.append(f'{inner}true') + + # 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 not None: + lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') + # Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст + if el.get('collapsedTitle'): + emit_mltext(lines, inner, 'CollapsedRepresentationTitle', el['collapsedTitle']) + + # United + if el.get('united') is False: + lines.append(f'{inner}false') + + # Формат значения пути к данным заголовка (; парный к titleDataPath группы) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Оформление (цвета/шрифты/граница) — перед компаньоном + emit_appearance(lines, el, inner, 'field') + + # 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, el.get('extendedTooltip')) + + # 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_column_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + group_val = str(el.get('columnGroup', '')) + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'inCell': 'InCell', + } + orientation = orientation_map.get(group_val) + if orientation: + lines.append(f'{inner}{orientation}') + + if el.get('showTitle') is not None: + lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') + # showInHeader эмитится общим emit_common_element_props (через emit_layout) + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD) + emit_column_pics(lines, el, inner) + + # Оформление (цвета/шрифты/граница) — перед компаньоном + emit_appearance(lines, el, inner, 'field') + + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + 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, auto=not el.get('path')) + 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 not None: + lines.append(f'{inner}{"true" if el["multiLine"] else "false"}') + if el.get('passwordMode') is not None: + lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') + # ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама, + # декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию). + if el.get('choiceButton') is not None: + lines.append(f'{inner}{"true" if el["choiceButton"] else "false"}') + # Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false) + if el.get('clearButton') is not None: + lines.append(f'{inner}{"true" if el["clearButton"] else "false"}') + if el.get('spinButton') is not None: + lines.append(f'{inner}{"true" if el["spinButton"] else "false"}') + if el.get('dropListButton') is not None: + lines.append(f'{inner}{"true" if el["dropListButton"] else "false"}') + if el.get('choiceListButton') is not None: + lines.append(f'{inner}{"true" if el["choiceListButton"] else "false"}') + if el.get('markIncomplete') is not None: + lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_column_pics(lines, el, inner) + if el.get('textEdit') is False: + lines.append(f'{inner}false') + # InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение) + for key, tag in (('wrap', 'Wrap'), ('openButton', 'OpenButton'), ('listChoiceMode', 'ListChoiceMode'), + ('extendedEditMultipleValues', 'ExtendedEditMultipleValues'), ('chooseType', 'ChooseType'), + ('quickChoice', 'QuickChoice'), ('autoChoiceIncomplete', 'AutoChoiceIncomplete')): + if el.get(key) is not None: + lines.append(f'{inner}<{tag}>{"true" if el[key] else "false"}') + # Ограничение доступных типов (поле на составном типе): домен типов + явный набор. + # availableTypes — формат типа реквизита (§type); emit_type сам разбирает мультитип "a | b". + if el.get('typeDomainEnabled') is not None: + lines.append(f'{inner}{"true" if el["typeDomainEnabled"] else "false"}') + if el.get('availableTypes'): + emit_type(lines, el['availableTypes'], inner, tag='AvailableTypes') + # InputField-специфичные value-скаляры + for key, tag in (('choiceForm', 'ChoiceForm'), ('choiceHistoryOnInput', 'ChoiceHistoryOnInput'), + ('choiceFoldersAndItems', 'ChoiceFoldersAndItems'), ('footerDataPath', 'FooterDataPath')): + if el.get(key): + lines.append(f'{inner}<{tag}>{esc_xml(str(el[key]))}') + # MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором). + for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue')): + if el.get(key) is not None: + mvt = 'xs:string' if isinstance(el[key], str) else 'xs:decimal' + lines.append(f'{inner}<{tag} xsi:type="{mvt}">{esc_xml(str(el[key]))}') + if el.get('choiceButtonRepresentation'): + lines.append(f'{inner}{el["choiceButtonRepresentation"]}') + emit_picture_ref(lines, el.get('choiceButtonPicture'), 'ChoiceButtonPicture', inner) + emit_layout(lines, el, inner, multi_line_default=(el.get('multiLine') is True)) + + if el.get('inputHint'): + emit_mltext(lines, inner, 'InputHint', el['inputHint']) + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + if el.get('footerText') is not None: + emit_mltext(lines, inner, 'FooterText', el['footerText']) + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + emit_choice_list(lines, el, inner) + + # Связи по типу / связи параметров выбора / параметры выбора + emit_type_link(lines, el, inner) + emit_choice_parameter_links(lines, el, inner) + emit_choice_parameters(lines, el, inner) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + 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, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_column_pics(lines, el, inner) + # CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг + _cbt_map = {'auto': 'Auto', 'checkbox': 'CheckBox', 'switcher': 'Switcher', 'tumbler': 'Tumbler'} + if 'checkBoxType' in el: + if el.get('checkBoxType'): + lines.append(f'{inner}{_cbt_map.get(str(el["checkBoxType"]).lower(), el["checkBoxType"])}') + else: + lines.append(f'{inner}Auto') + + emit_title_location(lines, el, inner, 'Right') + + emit_layout(lines, el, inner) + + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'check') + + lines.append(f'{indent}') + + +def emit_radio_button_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, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_title_location(lines, el, inner, 'None') + + rbt = normalize_radio_button_type(el.get('radioButtonType')) + lines.append(f'{inner}{rbt}') + + if el.get('columnsCount') is not None: + lines.append(f'{inner}{el["columnsCount"]}') + + emit_choice_list(lines, el, inner) + + emit_layout(lines, el, inner) + + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'radio') + + lines.append(f'{indent}') + + +# Заголовок декорации (Label/Picture): formatted-aware через единую ML-text форму +# (reuse resolve_ml_formatted, как у extendedTooltip). Sibling-ключ formatted — back-compat override. +def emit_decoration_title(lines, el, name, indent, auto=False): + has_key = 'title' in el + title_val = el['title'] if has_key else (title_from_name(name) if (auto and name) else None) + if title_val: + text, fmt = resolve_ml_formatted(title_val) + if 'formatted' in el: + fmt = bool(el['formatted']) + lines.append(f'{indent}<Title formatted="{"true" if fmt else "false"}">') + emit_ml_items(lines, f'{indent}\t', text) + lines.append(f'{indent}') + if el.get('tooltip'): + emit_mltext(lines, indent, 'ToolTip', el['tooltip']) + if el.get('tooltipRepresentation'): + lines.append(f'{indent}{el["tooltipRepresentation"]}') + + +def emit_label(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + # Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title + # (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title). + emit_common_flags(lines, el, inner) + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + emit_appearance(lines, el, inner, 'decoration') + + emit_decoration_title(lines, el, name, inner, auto=True) + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + 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, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + # FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode + if el.get('footerDataPath'): + lines.append(f'{inner}{esc_xml(str(el["footerDataPath"]))}') + # PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение + if el.get('passwordMode') is not None: + lines.append(f'{inner}{"true" if el["passwordMode"] else "false"}') + emit_column_pics(lines, el, inner) + # ВНИМАНИЕ: у LabelField платформенный тег (опечатка 1С), не . + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + if el.get('warningOnEdit') is not None: + emit_mltext(lines, inner, 'WarningOnEdit', el['warningOnEdit']) + if el.get('footerText') is not None: + emit_mltext(lines, inner, 'FooterText', el['footerText']) + + # Формат / формат редактирования (LocalStringType — строка или {ru,en}) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + + # Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'labelField') + + lines.append(f'{indent}') + + +# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C). +def emit_dynlist_table_block(lines, el, indent): + # (useAlternationRowColor — общее свойство таблицы, эмитится в emit_table) + # Group A (гарант. блок): дефолт + override + ar = 'true' if el.get('autoRefresh') is True else 'false' + lines.append(f'{indent}{ar}') + arp = el['autoRefreshPeriod'] if el.get('autoRefreshPeriod') is not None else 60 + lines.append(f'{indent}{arp}') + lines.append(f'{indent}') + lines.append(f'{indent}\tCustom') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}') + cfi = el.get('choiceFoldersAndItems') or 'Items' + lines.append(f'{indent}{cfi}') + rcr = 'true' if el.get('restoreCurrentRow') is True else 'false' + lines.append(f'{indent}{rcr}') + lines.append(f'{indent}') + sr = 'false' if el.get('showRoot') is False else 'true' + lines.append(f'{indent}{sr}') + arc = 'true' if el.get('allowRootChoice') is True else 'false' + lines.append(f'{indent}{arc}') + uodc = el.get('updateOnDataChange') or 'Auto' + lines.append(f'{indent}{uodc}') + if el.get('userSettingsGroup'): + lines.append(f'{indent}{el["userSettingsGroup"]}') + agcru = 'false' if el.get('allowGettingCurrentRowURL') is False else 'true' + lines.append(f'{indent}{agcru}') + + +def emit_table(lines, el, name, eid, indent): + _current_table_name['name'] = name # дефолт source для кастомных дополнений в commandBar + lines.append(f'{indent}
') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + # ChangeRowSet/Order — явное значение (в т.ч. false: платформа пишет его на ValueTable) + if 'changeRowSet' in el and el['changeRowSet'] is not None: + lines.append(f'{inner}{"true" if el["changeRowSet"] is True else "false"}') + if 'changeRowOrder' in el and el['changeRowOrder'] is not None: + lines.append(f'{inner}{"true" if el["changeRowOrder"] is True else "false"}') + if el.get('autoInsertNewRow') is True: + lines.append(f'{inner}true') + # RowFilter — nil-плейсхолдер (ключ присутствует → эмитим) + if 'rowFilter' in el: + lines.append(f'{inner}') + # Высота в строках () — отдельное свойство от (высота элемента, + # эмитится generic-ом emit_layout ниже). Таблица может нести оба (237 в корпусе). + if el.get('heightInTableRows'): + lines.append(f'{inner}{el["heightInTableRows"]}') + 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') + # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill). + if el.get('autofill') is not None: + lines.append(f'{inner}{"true" if el["autofill"] else "false"}') + if el.get('multipleChoice') is True: + lines.append(f'{inner}true') + if el.get('searchOnInput'): + lines.append(f'{inner}{el["searchOnInput"]}') + if el.get('markIncomplete') is not None: + lines.append(f'{inner}{"true" if el["markIncomplete"] else "false"}') + # Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table) + if el.get('headerHeight') is not None: + lines.append(f'{inner}{el["headerHeight"]}') + if el.get('footerHeight') is not None: + lines.append(f'{inner}{el["footerHeight"]}') + if el.get('useAlternationRowColor') is True: + lines.append(f'{inner}true') + if el.get('selectionMode'): + lines.append(f'{inner}{el["selectionMode"]}') + if el.get('rowSelectionMode'): + lines.append(f'{inner}{el["rowSelectionMode"]}') + if el.get('verticalLines') is False: + lines.append(f'{inner}false') + if el.get('horizontalLines') is False: + lines.append(f'{inner}false') + if el.get('initialTreeView'): + lines.append(f'{inner}{el["initialTreeView"]}') + if el.get('enableDrag') is not None: + lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') + if el.get('rowPictureDataPath'): + lines.append(f'{inner}{el["rowPictureDataPath"]}') + # RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel) + emit_picture_ref(lines, el.get('rowsPicture'), 'RowsPicture', inner) + # Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка) + if el.get('currentRowUse'): + lines.append(f'{inner}{el["currentRowUse"]}') + # Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop) + if el.get('refreshRequest'): + lines.append(f'{inner}{el["refreshRequest"]}') + # Блок свойств дин-список-таблицы (помечена эвристикой) + if el.get('_dynList'): + emit_dynlist_table_block(lines, el, inner) + if el.get('viewStatusLocation'): + lines.append(f'{inner}{el["viewStatusLocation"]}') + if el.get('searchControlLocation'): + lines.append(f'{inner}{el["searchControlLocation"]}') + emit_layout(lines, el, inner) + + # CommandSet таблицы эмитится через emit_layout (общий механизм поля) + + # Оформление (цвета/граница таблицы) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + # AutoCommandBar — with optional Autofill control + if el.get('commandBar') is not None: + emit_companion_panel(lines, 'AutoCommandBar', f'{name}\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c', inner, el.get('commandBar')) + elif 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, 'ExtendedTooltip', f'{name}\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f\u041f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430', inner, el.get('extendedTooltip')) + adds = el.get('additions') + emit_table_addition(lines, 'searchString', name, inner, get_addition_override(adds, 'searchString')) + emit_table_addition(lines, 'viewStatus', name, inner, get_addition_override(adds, 'viewStatus')) + emit_table_addition(lines, 'searchControl', name, inner, get_addition_override(adds, 'searchControl')) + + # 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' + + emit_title(lines, el, name, inner) + + if el.get('pagesRepresentation'): + lines.append(f'{inner}{el["pagesRepresentation"]}') + + emit_common_flags(lines, el, inner) + emit_layout(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, el.get('extendedTooltip')) + + 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, auto=True) + emit_common_flags(lines, el, inner) + + # Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD). + # Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект. + emit_picture_ref(lines, el.get('picture'), 'Picture', inner) + + if el.get('group'): + orientation_map = { + 'horizontal': 'Horizontal', + 'vertical': 'Vertical', + 'alwaysHorizontal': 'AlwaysHorizontal', + 'alwaysVertical': 'AlwaysVertical', + 'horizontalIfPossible': 'HorizontalIfPossible', + } + orientation = orientation_map.get(str(el['group'])) + if orientation: + lines.append(f'{inner}{orientation}') + if el.get('showTitle') is not None: + lines.append(f'{inner}{"true" if el["showTitle"] else "false"}') + # Формат значения пути к данным заголовка (; парный к titleDataPath страницы) + if el.get('format'): + emit_mltext(lines, inner, 'Format', el['format']) + if el.get('editFormat'): + emit_mltext(lines, inner, 'EditFormat', el['editFormat']) + emit_layout(lines, el, inner) + + # \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (BackColor / TitleTextColor / TitleFont) \u2014 \u043f\u043e\u0441\u043b\u0435 ShowTitle, \u043f\u0435\u0440\u0435\u0434 \u043a\u043e\u043c\u043f\u0430\u043d\u044c\u043e\u043d\u043e\u043c + emit_appearance(lines, el, inner, 'field') + + # 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, el.get('extendedTooltip')) + + # 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, in_cmd_bar=False): + lines.append(f'{indent}') + + +def emit_picture_decoration(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_decoration_title(lines, el, name, inner) + # Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса) + if el.get('nonselectedPictureText') is not None: + emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) + emit_common_flags(lines, el, inner) + + # Источник картинки — ТОЛЬКО src (ключ 'picture' = тип/имя элемента, не источник). + # Префикс "abs:" → встроенная картинка ; иначе именованная/стилевая . + if el.get('src'): + src_str = str(el['src']) + lt = 'true' if el.get('loadTransparent') is True else 'false' + lines.append(f'{inner}') + if src_str.startswith('abs:'): + lines.append(f'{inner}\t{esc_xml(src_str[4:])}') + else: + lines.append(f'{inner}\t{esc_xml(src_str)}') + lines.append(f'{inner}\t{lt}') + tpx = el.get('transparentPixel') + if tpx: + lines.append(f'{inner}\t') + lines.append(f'{inner}') + + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + emit_layout(lines, el, inner) + + # Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance) + emit_appearance(lines, el, inner, 'decoration') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + 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('editMode'): + lines.append(f'{inner}{el["editMode"]}') + emit_column_pics(lines, el, inner) + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + if el.get('hyperlink') is True: + lines.append(f'{inner}true') + + emit_layout(lines, el, inner) + + # ValuesPicture — picture (collection) used to render the field's value. + # Required for a Boolean-bound PictureField to actually show an icon. + # Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда. + emit_picture_ref(lines, el.get('valuesPicture'), 'ValuesPicture', inner) + if el.get('nonselectedPictureText') is not None: + emit_mltext(lines, inner, 'NonselectedPictureText', el['nonselectedPictureText']) + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, 'picField') + + lines.append(f'{indent}') + + +def emit_simple_field(lines, el, name, eid, indent, xml_tag, type_key): + # Спец-поля "документ/датчик" (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar): + # единый скелет поля. Типоспец. enum/bool скаляры — через generic (emit_layout); + # числовые скаляры датчиков (min/max/шаги) — без xsi:type; enableDrag — фактическое значение. + lines.append(f'{indent}<{xml_tag} name="{name}" id="{eid}"{di_attr(el)}>') + inner = f'{indent}\t' + + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + if el.get('editMode'): + lines.append(f'{inner}{el["editMode"]}') + + emit_layout(lines, el, inner) + + # EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через emit_layout. + if el.get('enableDrag') is not None: + lines.append(f'{inner}{"true" if el["enableDrag"] else "false"}') + + # Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type) + for key, tag in (('minValue', 'MinValue'), ('maxValue', 'MaxValue'), ('largeStep', 'LargeStep'), ('markingStep', 'MarkingStep'), ('step', 'Step')): + if el.get(key) is not None: + lines.append(f'{inner}<{tag}>{el[key]}') + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + emit_events(lines, el, name, inner, type_key) + + lines.append(f'{indent}') + + +def emit_gantt_chart(lines, el, name, eid, indent): + # GanttChartField — скелет поля + вложенная (полноценная таблица, через emit_element). + lines.append(f'{indent}') + inner = f'{indent}\t' + if el.get('path'): + lines.append(f'{inner}{el["path"]}') + emit_title(lines, el, name, inner, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + if el.get('titleLocation'): + lines.append(f'{inner}{map_title_loc(el["titleLocation"])}') + emit_layout(lines, el, inner) + emit_appearance(lines, el, inner, 'field') + emit_companion_panel(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner, el.get('contextMenu')) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + # Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем emit_element) + if el.get('ganttTable'): + emit_element(lines, el['ganttTable'], inner) + emit_events(lines, el, name, inner, 'ganttChart') + 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, auto=not el.get('path')) + emit_common_flags(lines, el, inner) + + if el.get('titleLocation'): + loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom', 'auto': 'Auto'} + loc = loc_map.get(str(el['titleLocation']), str(el['titleLocation'])) + lines.append(f'{inner}{loc}') + + emit_layout(lines, el, inner) + + # Календарно-специфичные свойства (порядок схемы: после layout, до companions) + if el.get('selectionMode'): + lines.append(f'{inner}{el["selectionMode"]}') + if el.get('showCurrentDate') is not None: + lines.append(f'{inner}{"true" if el["showCurrentDate"] else "false"}') + if el.get('widthInMonths') is not None: + lines.append(f'{inner}{el["widthInMonths"]}') + if el.get('heightInMonths') is not None: + lines.append(f'{inner}{el["heightInMonths"]}') + if el.get('showMonthsPanel') is not None: + lines.append(f'{inner}{"true" if el["showMonthsPanel"] else "false"}') + + # Оформление (цвета/шрифты/граница) — перед компаньонами + emit_appearance(lines, el, inner, 'field') + + # Companions + emit_companion_panel(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner, el.get('contextMenu')) + 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, el.get('extendedTooltip')) + + 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' + + emit_title(lines, el, name, inner) + + if el.get('commandSource'): + lines.append(f'{inner}{el["commandSource"]}') + + if el.get('autofill') is True: + lines.append(f'{inner}true') + + _hl = get_hlocation(el) + if _hl: + lines.append(f'{inner}{_hl}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + # 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', in_cmd_bar=True) + 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, auto=True) + emit_common_flags(lines, el, inner) + + emit_command_picture(lines, el.get('picture'), el.get('loadTransparent'), inner) + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + emit_layout(lines, el, inner) + + # Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном + emit_appearance(lines, el, inner, 'field') + + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + # 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', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +def emit_button_group(lines, el, name, eid, indent): + lines.append(f'{indent}') + inner = f'{indent}\t' + + emit_title(lines, el, name, inner) + + if el.get('commandSource'): + lines.append(f'{inner}{el["commandSource"]}') + + if el.get('representation'): + lines.append(f'{inner}{el["representation"]}') + + emit_common_flags(lines, el, inner) + emit_layout(lines, el, inner) + + # Companion: ExtendedTooltip + emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner, el.get('extendedTooltip')) + + # 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', in_cmd_bar=True) + lines.append(f'{inner}') + + lines.append(f'{indent}') + + +# --- Attribute emitter --- + +def emit_functional_options(lines, fo, indent): + # FunctionalOption.X…> — у Attribute/Command/Column. + # Forgiving: "X"/"FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть. + if not fo: + return + lines.append(f'{indent}') + for opt in fo: + v = str(opt) + if re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$', v): + pass + elif v.startswith('FunctionalOption.'): + pass + else: + v = f'FunctionalOption.{v}' + lines.append(f'{indent}\t{v}') + lines.append(f'{indent}') + + +def emit_attr_column(lines, col, indent): + # Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions. + col_id = new_id() + lines.append(f'{indent}') + if col.get('title'): + emit_mltext(lines, f'{indent}\t', 'Title', col['title']) + emit_type(lines, str(col.get('type', '')), f'{indent}\t') + emit_functional_options(lines, col.get('functionalOptions'), f'{indent}\t') + # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита + if col.get('view') is not None: + emit_xr_flag(lines, 'View', col['view'], f'{indent}\t') + if col.get('edit') is not None: + emit_xr_flag(lines, 'Edit', col['edit'], f'{indent}\t') + lines.append(f'{indent}') + + +# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) --- +# Зеркало form-compile.ps1 (Emit-DLParameters). Та же сущность, что параметры СКД, но в +# форме: обёртка + дети dcssch:. DSL переиспользует грамматику параметров СКД. +# Контекстные дефолты: useRestriction эмитим ВСЕГДА, дефолт true (в СКД false); title — авто +# из имени; пустое value — всегда xsi:nil (даже при известном типе). Канон. порядок детей +# (по корпусу): name, title, valueType, value, useRestriction, expression, availableValue*, +# valueListAllowed, availableAsField, inputParameters, denyIncompleteValues, use. + +def emit_dl_mltext(lines, indent, tag, text): + # ML-текст с xsi:type="v8:LocalStringType" (в dcssch:* обязателен; emit_mltext его не ставит). + lines.append(f'{indent}<{tag} xsi:type="v8:LocalStringType">') + emit_ml_items(lines, f'{indent}\t', text) + lines.append(f'{indent}') + + +def split_dl_valuelist_csv(s): + result = [] + if s is None: + return result + items = [] + buf = [] + in_quote = None + for ch in s: + if in_quote: + buf.append(ch) + if ch == in_quote: + in_quote = None + elif ch in ("'", '"'): + in_quote = ch + buf.append(ch) + elif ch == ',': + items.append(''.join(buf)); buf = [] + else: + buf.append(ch) + if buf: + items.append(''.join(buf)) + for raw in items: + t = raw.strip() + if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')): + t = t[1:-1] + if t != '': + result.append(t) + return result + + +def parse_dl_param_shorthand(s): + result = {'name': '', 'type': '', 'value': None, 'title': None} + if '@valueList' in s: + result['valueListAllowed'] = True + s = re.sub(r'\s*@valueList', '', s) + if '@hidden' in s: + result['hidden'] = True + s = re.sub(r'\s*@hidden', '', s) + m = re.search(r'\[([^\]]*)\]', s) + if m: + result['title'] = m.group(1).strip() + s = re.sub(r'\s*\[[^\]]*\]\s*', ' ', s).strip() + # Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит). + m = re.match(r'^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$', s) + if m: + result['name'] = m.group(1).strip() + type_raw = m.group(2).strip() + if re.search(r'[|+]', type_raw): + result['type'] = ' | '.join(resolve_type_str(p.strip()) for p in re.split(r'\s*[|+]\s*', type_raw)) + else: + result['type'] = resolve_type_str(type_raw) + if m.group(4): + rhs = m.group(4).strip() + items = split_dl_valuelist_csv(rhs) + if len(items) >= 2: + result['value'] = items + result['valueListAllowed'] = True + elif len(items) == 1: + result['value'] = items[0] + else: + result['value'] = rhs + else: + result['name'] = s.strip() + return result + + +def is_dl_empty_value(v): + if v is None: + return True + sv = str(v).strip() + return sv == '' or sv == '_' or sv.lower() == 'null' + + +def emit_dl_value(lines, type_str, val, indent, value_list_allowed=False): + if is_dl_empty_value(val): + # Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil (даже при известном типе). + if value_list_allowed: + return + lines.append(f'{indent}') + return + if isinstance(val, bool): + val_str = 'true' if val else 'false' + else: + val_str = str(val) + t = type_str or '' + if re.match(r'^(date|dateTime|time)', t): + lines.append(f'{indent}{esc_xml(val_str)}') + elif t == 'boolean': + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^decimal', t): + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^string', t): + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t): + lines.append(f'{indent}{esc_xml(val_str)}') + else: + if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): + lines.append(f'{indent}{esc_xml(val_str)}') + elif val_str in ('true', 'false'): + lines.append(f'{indent}{esc_xml(val_str)}') + elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str): + lines.append(f'{indent}{esc_xml(val_str)}') + else: + lines.append(f'{indent}{esc_xml(val_str)}') + + +def emit_dl_value_type(lines, type_str, indent): + if not type_str: + return + lines.append(f'{indent}') + for part in re.split(r'\s*[|+]\s*', str(type_str)): + emit_single_type(lines, part.strip(), f'{indent}\t') + lines.append(f'{indent}') + + +def emit_dl_available_value(lines, av, type_str, indent): + lines.append(f'{indent}') + av_val = av.get('value') if isinstance(av, dict) else None + emit_dl_value(lines, type_str, av_val, f'{indent}\t', False) + pres = (av.get('presentation') or av.get('title')) if isinstance(av, dict) else None + if pres: + emit_dl_mltext(lines, f'{indent}\t', 'dcssch:presentation', pres) + lines.append(f'{indent}') + + +def emit_dl_input_parameters(lines, ip, indent): + if ip is None: + return + items = ip if isinstance(ip, list) else [ip] + if len(items) == 0: + return + lines.append(f'{indent}') + for item in items: + lines.append(f'{indent}\t') + if 'use' in item and item.get('use') is not None and not item.get('use'): + lines.append(f'{indent}\t\tfalse') + lines.append(f'{indent}\t\t{esc_xml(str(item.get("parameter", "")))}') + if 'choiceParameters' in item: + cp_items = item.get('choiceParameters') or [] + if len(cp_items) == 0: + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + for cp in cp_items: + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t\t\t{esc_xml(str(cp.get("name", "")))}') + for v in (cp.get('values') or []): + if isinstance(v, bool): + lines.append(f'{indent}\t\t\t\t{"true" if v else "false"}') + elif isinstance(v, (int, float)): + lines.append(f'{indent}\t\t\t\t{v}') + else: + lines.append(f'{indent}\t\t\t\t{esc_xml(str(v))}') + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t') + elif 'choiceParameterLinks' in item: + cpl_items = item.get('choiceParameterLinks') or [] + if len(cpl_items) == 0: + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + for cpl in cpl_items: + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("name", "")))}') + lines.append(f'{indent}\t\t\t\t{esc_xml(str(cpl.get("value", "")))}') + mode = str(cpl.get('mode') or 'Auto') + lines.append(f'{indent}\t\t\t\t{mode}') + lines.append(f'{indent}\t\t\t') + lines.append(f'{indent}\t\t') + elif 'value' in item: + val = item.get('value') + if isinstance(val, bool): + lines.append(f'{indent}\t\t{"true" if val else "false"}') + elif isinstance(val, (int, float)): + lines.append(f'{indent}\t\t{val}') + elif isinstance(val, dict): + emit_dl_mltext(lines, f'{indent}\t\t', 'dcscor:value', val) + else: + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd ── +def _test_empty_value(v): + if v is None: + return True + s = str(v).strip() + return s == '' or s == '_' or s.lower() == 'null' + + +def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False): + if value_list_allowed: + return + t = type_str or '' + t_bare = t[3:] if t.startswith('xs:') else t + pf = tag_prefix + if t == '': + lines.append(f'{indent}<{pf}value xsi:nil="true"/>') + elif t == 'StandardPeriod': + lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">') + lines.append(f'{indent}\tCustom') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}') + elif re.match(r'^string', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>') + elif re.match(r'^(date|time)', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00') + elif re.match(r'^decimal', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0') + elif t_bare == 'boolean': + lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false') + else: + lines.append(f'{indent}<{pf}value xsi:nil="true"/>') + + +_DP_PERIOD_VARIANTS = {"Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear"} + + +def parse_data_param_shorthand(s): + result = {'parameter': '', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None} + if '@user' in s: + result['userSettingID'] = 'auto'; s = re.sub(r'\s*@user', '', s) + if '@off' in s: + result['use'] = False; s = re.sub(r'\s*@off', '', s) + if '@quickAccess' in s: + result['viewMode'] = 'QuickAccess'; s = re.sub(r'\s*@quickAccess', '', s) + if '@normal' in s: + result['viewMode'] = 'Normal'; s = re.sub(r'\s*@normal', '', s) + s = s.strip() + m = re.match(r'^([^=]+)=\s*(.+)$', s) + if m: + result['parameter'] = m.group(1).strip() + val_str = m.group(2).strip() + if val_str in _DP_PERIOD_VARIANTS: + result['value'] = {'variant': val_str} + elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): + result['value'] = val_str + elif val_str in ('true', 'false'): + result['value'] = (val_str == 'true') + else: + result['value'] = val_str + else: + result['parameter'] = s + return result + + +def emit_data_parameters(lines, items, indent, block_view_mode=None): + if not items or len(items) == 0: + return + lines.append(f'{indent}') + for dp in items: + if isinstance(dp, str): + parsed = parse_data_param_shorthand(dp) + dp = {'parameter': parsed['parameter']} + if parsed['value'] is not None: + dp['value'] = parsed['value'] + if parsed['use'] is False: + dp['use'] = False + if parsed['userSettingID']: + dp['userSettingID'] = parsed['userSettingID'] + if parsed['viewMode']: + dp['viewMode'] = parsed['viewMode'] + lines.append(f'{indent}\t') + if dp.get('use') is False: + lines.append(f'{indent}\t\tfalse') + lines.append(f'{indent}\t\t{esc_xml(str(dp.get("parameter", "")))}') + vtype = str(dp.get('valueType') or '') + val = dp.get('value') + if dp.get('nilValue') is True: + lines.append(f'{indent}\t\t') + elif _test_empty_value(val) and vtype: + emit_empty_value(lines, vtype, f'{indent}\t\t', tag_prefix='dcscor:', value_list_allowed=False) + elif _test_empty_value(val): + pass # нет значения → не эмитим value-узел (form дин-список: use=false плейсхолдер) + elif val is not None: + if isinstance(val, dict) and val.get('variant'): + variant = str(val.get('variant')) + has_date = 'date' in val + has_sd = 'startDate' in val + is_sbd = has_date or (not has_sd and variant.startswith('BeginningOf')) + if is_sbd: + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(variant)}') + if variant == 'Custom': + d = str(val.get('date') or '0001-01-01T00:00:00') + lines.append(f'{indent}\t\t\t{esc_xml(d)}') + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(variant)}') + if variant == 'Custom': + sd = str(val.get('startDate') or '0001-01-01T00:00:00') + ed = str(val.get('endDate') or '0001-01-01T00:00:00') + lines.append(f'{indent}\t\t\t{esc_xml(sd)}') + lines.append(f'{indent}\t\t\t{esc_xml(ed)}') + lines.append(f'{indent}\t\t') + elif re.match(r'^[a-zA-Z]+:', vtype): + v_str = str(val).lower() if isinstance(val, bool) else str(val) + lines.append(f'{indent}\t\t{esc_xml(v_str)}') + elif vtype == 'boolean' or isinstance(val, bool): + lines.append(f'{indent}\t\t{esc_xml(str(val).lower())}') + elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^decimal', vtype): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^string', vtype): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + else: + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + if dp.get('viewMode'): + lines.append(f'{indent}\t\t{esc_xml(str(dp["viewMode"]))}') + if dp.get('userSettingID'): + uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID']) + lines.append(f'{indent}\t\t{esc_xml(uid)}') + if dp.get('userSettingPresentation'): + emit_us_presentation(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation']) + lines.append(f'{indent}\t') + if block_view_mode is not None: + lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') + lines.append(f'{indent}') + + +def emit_dl_parameter(lines, p, parsed, indent): + is_obj = not isinstance(p, str) + lines.append(f'{indent}') + ci = f'{indent}\t' + lines.append(f'{ci}{esc_xml(parsed["name"])}') + # Title: явный override (shorthand [..] / объект title/presentation) или авто из имени. + title = None + if parsed.get('title'): + title = parsed['title'] + elif is_obj and p.get('title'): + title = p['title'] + elif is_obj and p.get('presentation'): + title = p['presentation'] + if title is None or (isinstance(title, str) and title == ''): + title = title_from_name(parsed['name']) + emit_dl_mltext(lines, ci, 'dcssch:title', title) + # valueType + if parsed.get('type'): + emit_dl_value_type(lines, parsed['type'], ci) + # value (дефолт nil; при valueListAllowed пустое — опускаем) + vla = bool(parsed.get('valueListAllowed')) + pv = parsed.get('value') + if isinstance(pv, list): + for v in pv: + emit_dl_value(lines, parsed.get('type', ''), v, ci, False) + elif vla and is_dl_empty_value(pv) and parsed.get('value_explicit'): + # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа пишет nil + lines.append(f'{ci}') + else: + emit_dl_value(lines, parsed.get('type', ''), pv, ci, vla) + # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. + ur = True + if is_obj and 'useRestriction' in p: + ur = bool(p['useRestriction']) + lines.append(f'{ci}{"true" if ur else "false"}') + # expression + expr = str(p['expression']) if (is_obj and p.get('expression')) else None + if expr: + lines.append(f'{ci}{esc_xml(expr)}') + # availableValues + if is_obj and p.get('availableValues'): + for av in p['availableValues']: + emit_dl_available_value(lines, av, parsed.get('type', ''), ci) + # valueListAllowed + if vla: + lines.append(f'{ci}true') + # availableAsField=false (hidden или явный) + aaf = None + if parsed.get('hidden') is True: + aaf = False + if is_obj and 'availableAsField' in p: + aaf = bool(p['availableAsField']) + if aaf is False: + lines.append(f'{ci}false') + # inputParameters + if is_obj and p.get('inputParameters'): + emit_dl_input_parameters(lines, p['inputParameters'], ci) + # denyIncompleteValues + if is_obj and p.get('denyIncompleteValues') is True: + lines.append(f'{ci}true') + # use + if is_obj and p.get('use'): + lines.append(f'{ci}{esc_xml(str(p["use"]))}') + lines.append(f'{indent}') + + +def emit_dl_parameters(lines, params, indent): + if not params: + return + for p in params: + if isinstance(p, str): + parsed = parse_dl_param_shorthand(p) + else: + resolved_type = '' + if p.get('type'): + if isinstance(p['type'], list): + resolved_type = ' | '.join(resolve_type_str(str(x)) for x in p['type']) + else: + resolved_type = resolve_type_str(str(p['type'])) + elif p.get('valueType'): + resolved_type = resolve_type_str(str(p['valueType'])) + parsed = {'name': str(p.get('name', '')), 'type': resolved_type, + 'value': p.get('value') if 'value' in p else None, + 'value_explicit': ('value' in p), 'title': None} + if p.get('valueListAllowed') is True: + parsed['valueListAllowed'] = True + if p.get('hidden') is True: + parsed['hidden'] = True + emit_dl_parameter(lines, p, parsed, indent) + + +def emit_attributes(lines, attrs, indent, conditional_appearance=None): + has_ca = bool(conditional_appearance) and len(conditional_appearance) > 0 + # Платформа ВСЕГДА эмитит (100% корпуса; 162 формы — пустой ). + if (not attrs or len(attrs) == 0) and not has_ca: + lines.append(f'{indent}') + return + if not attrs or len(attrs) == 0: + # Нет реквизитов, но есть условное оформление (последний child ) + lines.append(f'{indent}') + emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') + lines.append(f'{indent}') + return + + lines.append(f'{indent}') + seen_attrs = set() + for attr in attrs: + attr_id = new_id() + attr_name = str(attr['name']) + _ensure_unique(attr_name, seen_attrs, 'attribute') + + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + # Title атрибута (зеркало emit_title): нет ключа → авто-вывод из имени (кроме main); + # title "" → подавить; непустой → эмитить как есть. + if 'title' in attr: + if attr.get('title'): + emit_mltext(lines, inner, 'Title', attr['title']) + elif attr.get('main') is not True: + emit_mltext(lines, inner, 'Title', title_from_name(attr_name)) + + # Type + if attr.get('type'): + emit_type(lines, str(attr['type']), inner) + else: + lines.append(f'{inner}') + # valueType: ОписаниеТипов значений ValueList → + # (та же грамматика типа, включая составной "A | B"). Forgiving-синонимы. + # Три состояния: нет ключа → нет Settings; "" → пустой ; тип → с типом. + vt_spec = None + has_vt = False + for k in ('valueType', 'typeDescription', 'описаниеТипов', 'типЗначений'): + if k in attr: + vt_spec = attr[k] + has_vt = True + break + if has_vt: + emit_type(lines, '' if vt_spec is None else str(vt_spec), inner, tag="Settings", tag_attrs=' xsi:type="v8:TypeDescription"') + # Planner design-time (встроенный конфиг планировщика). + if attr.get('planner') is not None: + emit_planner_settings(lines, attr['planner'], inner) + # Chart/GanttChart design-time (тип выводится из типа реквизита). + if attr.get('chart') is not None: + ctype = 'd4p1:GanttChart' if 'GanttChart' in str(attr.get('type', '')) else 'd4p1:Chart' + emit_chart_settings(lines, attr['chart'], inner, ctype) + + if attr.get('main') is True: + lines.append(f'{inner}true') + # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) + if attr.get('view') is not None: + emit_xr_flag(lines, 'View', attr.get('view'), inner) + if attr.get('edit') is not None: + emit_xr_flag(lines, 'Edit', attr.get('edit'), inner) + main_saved = False + if attr.get('main') is True and attr.get('type'): + t = str(attr['type']) + main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) + # Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто. + emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved + if emit_saved: + lines.append(f'{inner}true') + # Save: сохранение значения реквизита в пользовательских настройках. true → имя; + # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). + # Нет ключа или false → не эмитим. + if 'save' in attr and attr['save'] is not None: + save_fields = [] + sv = attr['save'] + if isinstance(sv, bool): + if sv: + save_fields.append(attr_name) + else: + for e in (sv if isinstance(sv, (list, tuple)) else [sv]): + fld = str(e) + if not fld: + continue + if fld != attr_name and '.' not in fld and not re.match(r'^\d+/\d+:', fld): + fld = f'{attr_name}.{fld}' + if fld not in save_fields: + save_fields.append(fld) + if save_fields: + lines.append(f'{inner}') + for f in save_fields: + lines.append(f'{inner}\t{esc_xml(f)}') + lines.append(f'{inner}') + # Проверка заполнения → (реальный тег; в схеме нет). + # bool true → ShowError; строка → verbatim. Синоним fillChecking. + fc_raw = attr['fillCheck'] if 'fillCheck' in attr else attr.get('fillChecking') + if fc_raw: + fcv = 'ShowError' if isinstance(fc_raw, bool) else str(fc_raw) + lines.append(f'{inner}{fcv}') + + # UseAlways: поля, всегда читаемые. Две формы DSL сливаются: + # attr.useAlways[] (короткие имена) + columns с useAlways:true → ИмяРеквизита.Поле. + ua_fields = [] + for e in (attr.get('useAlways') or []): + fld = str(e) + # Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~" + # (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен. + # Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод). + if fld.startswith('~'): + bare = fld[1:] + if not re.match(r'^' + re.escape(attr_name) + r'\.', bare): + bare = f'{attr_name}.{bare}' + fld = f'~{bare}' + elif not re.match(r'^' + re.escape(attr_name) + r'\.', fld): + fld = f'{attr_name}.{fld}' + if fld not in ua_fields: + ua_fields.append(fld) + for col in (attr.get('columns') or []): + if col.get('useAlways') is True: + fld = f'{attr_name}.{col["name"]}' + if fld not in ua_fields: + ua_fields.append(fld) + if ua_fields: + lines.append(f'{inner}') + for f in ua_fields: + lines.append(f'{inner}\t{f}') + lines.append(f'{inner}') + + emit_functional_options(lines, attr.get('functionalOptions'), inner) + + # Columns: прямые + (доп. колонки табличных частей объекта). + # Прямые сначала, затем AdditionalColumns-группы. Для дин-списка (settings) прямые НЕ эмитим. + has_direct_cols = bool(attr.get('columns')) and len(attr['columns']) > 0 and not attr.get('settings') + has_add_cols = bool(attr.get('additionalColumns')) and len(attr['additionalColumns']) > 0 + if has_direct_cols or has_add_cols: + lines.append(f'{inner}') + if has_direct_cols: + seen_cols = set() # колонки уникальны в пределах своего реквизита + for col in attr['columns']: + _ensure_unique(str(col['name']), seen_cols, f"column of '{attr_name}'") + emit_attr_column(lines, col, f'{inner}\t') + if has_add_cols: + for ac in attr['additionalColumns']: + ac_cols = ac.get('columns') or [] + if not ac_cols: + # Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа) + lines.append(f'{inner}\t') + continue + lines.append(f'{inner}\t') + seen_ac_cols = set() # уникальность в пределах группы AdditionalColumns + for col in ac_cols: + _ensure_unique(str(col['name']), seen_ac_cols, f"column of '{attr_name}'") + emit_attr_column(lines, col, f'{inner}\t\t') + lines.append(f'{inner}\t') + lines.append(f'{inner}') + + # Settings (динамический список) + if attr.get('settings'): + s = attr['settings'] + lines.append(f'{inner}') + si = f'{inner}\t' + # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). + if s.get('autoFillAvailableFields') is not None: + lines.append(f'{si}{"true" if s["autoFillAvailableFields"] else "false"}') + # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + has_query = bool(s.get('query') and str(s['query']).strip()) + mq = 'true' if (has_query or s.get('manualQuery')) else 'false' + lines.append(f'{si}{mq}') + # DynamicDataRead: дефолт true; false только при явном отключении + ddr = 'false' if s.get('dynamicDataRead') is False else 'true' + lines.append(f'{si}{ddr}') + if has_query: + qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR) + lines.append(f'{si}{esc_xml(qtext)}') + # Явные поля набора (редко): override title/dataPath + if s.get('fields'): + for fld in s['fields']: + # Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet + # (поле-вложенный набор = реквизит табличной части; маркер nested). + ftype = 'DataSetFieldNestedDataSet' if fld.get('nested') else 'DataSetFieldField' + lines.append(f'{si}') + dp = fld.get('dataPath') or fld.get('field') + lines.append(f'{si}\t{esc_xml(str(dp))}') + lines.append(f'{si}\t{esc_xml(str(fld.get("field", "")))}') + if fld.get('title'): + lines.append(f'{si}\t') + emit_ml_items(lines, f'{si}\t\t', fld['title']) + lines.append(f'{si}\t') + lines.append(f'{si}') + # Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable. + emit_dl_parameters(lines, s.get('parameters'), si) + if s.get('mainTable'): + lines.append(f'{si}{normalize_meta_type_ref(str(s["mainTable"]))}') + # AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении). + if s.get('autoSaveUserSettings') is not None: + lines.append(f'{si}{"true" if s["autoSaveUserSettings"] else "false"}') + # ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID. + # Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы. + lsi = f'{si}\t' + lines.append(f'{si}') + ls_open_idx = len(lines) - 1 # для self-closing, если внутри ничего не эмитнётся + ls_shape = s.get('listSettings') + if ls_shape is not None: + # Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой. + for tag, meta in ls_shape.items(): + meta = str(meta) + bvm = 'Normal' if 'v' in meta else None + if tag == 'filter': + bus = CANON_FILTER_ID if 'u' in meta else None + emit_filter(lines, s.get('filter'), lsi, block_view_mode=bvm, block_user_setting_id=bus) + elif tag == 'order': + bus = CANON_ORDER_ID if 'u' in meta else None + emit_order(lines, s.get('order'), lsi, block_view_mode=bvm, block_user_setting_id=bus) + elif tag == 'conditionalAppearance': + bus = CANON_CA_ID if 'u' in meta else None + emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode=bvm, block_user_setting_id=bus) + elif tag == 'itemsViewMode': + lines.append(f'{lsi}Normal') + elif tag == 'itemsUserSettingID': + lines.append(f'{lsi}{CANON_ITEMS_ID}') + else: + # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. + emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) + # dataParameters — после filter, до order (XSD-порядок ListSettings) + if 'dataParameters' in s: + emit_data_parameters(lines, s.get('dataParameters'), lsi) + emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID) + emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) + lines.append(f'{lsi}Normal') + lines.append(f'{lsi}{CANON_ITEMS_ID}') + if len(lines) - 1 == ls_open_idx: + # Пустой дескриптор listSettings:{} (оригинал = ) → зеркалим self-closing. + lines[ls_open_idx] = f'{si}' + else: + lines.append(f'{si}') + lines.append(f'{inner}') + + lines.append(f'{indent}\t') + # Условное оформление формы — последний child (та же DCS-грамматика, что settings CA) + emit_conditional_appearance(lines, conditional_appearance, f'{indent}\t', wrap_tag='ConditionalAppearance') + lines.append(f'{indent}') + + +# --- Parameter emitter --- + +def emit_parameters(lines, params, indent): + if not params or len(params) == 0: + return + + lines.append(f'{indent}') + seen_params = set() + for param in params: + _ensure_unique(str(param['name']), seen_params, 'parameter') + 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}') + seen_cmds = set() + for cmd in cmds: + cmd_id = new_id() + _ensure_unique(str(cmd['name']), seen_cmds, 'command') + lines.append(f'{indent}\t') + inner = f'{indent}\t\t' + + # Заголовок команды (зеркало emit_title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс + # (в оригинале нет — не додумывать); ключ отсутствует → авто-вывод из имени. + if 'title' in cmd: + if cmd['title']: + emit_mltext(lines, inner, 'Title', cmd['title']) + else: + cmd_title = title_from_name(str(cmd['name'])) + if cmd_title: + emit_mltext(lines, inner, 'Title', cmd_title) + + if cmd.get('tooltip'): + emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) + + # Доступность команды по ролям (после ToolTip, до Action) + if cmd.get('use') is not None: + emit_xr_flag(lines, 'Use', cmd.get('use'), inner) + + if cmd.get('action'): + lines.append(f'{inner}<Action>{cmd["action"]}</Action>') + + if cmd.get('modifiesSavedData') is True: + lines.append(f'{inner}<ModifiesSavedData>true</ModifiesSavedData>') + + emit_functional_options(lines, cmd.get('functionalOptions'), inner) + + if cmd.get('currentRowUse'): + lines.append(f'{inner}<CurrentRowUse>{cmd["currentRowUse"]}</CurrentRowUse>') + + # Используемая таблица — имя элемента-таблицы (xsi:type обязателен). + # Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.) + _cmd_norm = {k.replace(' ', '').lower(): v for k, v in cmd.items()} + cmd_table = (_cmd_norm.get('table') or _cmd_norm.get('associatedtableelementid') + or _cmd_norm.get('используемаятаблица')) + if cmd_table: + lines.append(f'{inner}<AssociatedTableElementId xsi:type="xs:string">{esc_xml(str(cmd_table))}</AssociatedTableElementId>') + + if cmd.get('shortcut'): + lines.append(f'{inner}<Shortcut>{cmd["shortcut"]}</Shortcut>') + + emit_command_picture(lines, cmd.get('picture'), cmd.get('loadTransparent'), inner) + + if cmd.get('representation'): + lines.append(f'{inner}<Representation>{cmd["representation"]}</Representation>') + + lines.append(f'{indent}\t</Command>') + lines.append(f'{indent}</Commands>') + + +# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel. +# Элемент: строка (голый command, Type=Auto) или dict. Порядок тегов: +# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag). +def _resolve_command_group_key(key, panel_tag): + """Ключ-группа древовидной формы → CommandGroup (зависит от панели); иначе verbatim.""" + k = re.sub(r'\s', '', str(key)).lower() + if panel_tag == 'NavigationPanel': + m = {'important': 'FormNavigationPanelImportant', 'важное': 'FormNavigationPanelImportant', + 'goto': 'FormNavigationPanelGoTo', 'перейти': 'FormNavigationPanelGoTo', + 'seealso': 'FormNavigationPanelSeeAlso', 'смтакже': 'FormNavigationPanelSeeAlso'} + else: + m = {'important': 'FormCommandBarImportant', 'важное': 'FormCommandBarImportant', + 'createbasedon': 'FormCommandBarCreateBasedOn', 'создатьнаосновании': 'FormCommandBarCreateBasedOn'} + return m.get(k, key) + + +def emit_command_interface(lines, ci, indent): + if not ci: + return + inner = f'{indent}\t' + panels = [ + ('CommandBar', ('commandBar', 'команднаяПанель', 'КоманднаяПанель')), + ('NavigationPanel', ('navigationPanel', 'панельНавигации', 'ПанельНавигации')), + ] + present = [] + for tag, syns in panels: + items = None + for syn in syns: + if isinstance(ci, dict) and syn in ci: + items = ci[syn] + break + if items is not None: + present.append((tag, items)) + if not present: + return + lines.append(f'{indent}<CommandInterface>') + for tag, items in present: + lines.append(f'{inner}<{tag}>') + # Нормализация: плоский список пар (элемент, group-из-дерева). dict → древовидная форма. + flat = [] + if isinstance(items, dict): + for gkey, gitems in items.items(): + grp_tree = _resolve_command_group_key(gkey, tag) + for it in gitems: + flat.append((it, grp_tree)) + else: + for it in items: + flat.append((it, None)) + for item, tree_group in flat: + if isinstance(item, str): + cmd, typ, attr, grp, idx, dv, vis = item, 'Auto', None, None, None, None, None + else: + cmd = get_el_prop(item, ('command', 'команда')) + typ = get_el_prop(item, ('type', 'тип')) or 'Auto' + attr = get_el_prop(item, ('attribute', 'реквизит')) + grp = get_el_prop(item, ('group', 'группа', 'группаКоманд')) + idx = get_el_prop(item, ('index', 'индекс')) + dv = get_el_prop(item, ('defaultVisible', 'видимость', 'видимостьПоУмолчанию')) + vis = get_el_prop(item, ('visible', 'видимостьПоРолям', 'настройкаВидимости')) + # group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк + if tree_group: + grp = tree_group + lines.append(f'{inner}\t<Item>') + lines.append(f'{inner}\t\t<Command>{esc_xml(str(cmd))}</Command>') + lines.append(f'{inner}\t\t<Type>{typ}</Type>') + if attr: + lines.append(f'{inner}\t\t<Attribute>{esc_xml(str(attr))}</Attribute>') + if grp: + lines.append(f'{inner}\t\t<CommandGroup>{esc_xml(str(grp))}</CommandGroup>') + if idx is not None: + lines.append(f'{inner}\t\t<Index>{idx}</Index>') + if dv is not None: + lines.append(f'{inner}\t\t<DefaultVisible>{"true" if dv else "false"}</DefaultVisible>') + if vis is not None: + emit_xr_flag(lines, 'Visible', vis, f'{inner}\t\t') + lines.append(f'{inner}\t</Item>') + lines.append(f'{inner}</{tag}>') + lines.append(f'{indent}</CommandInterface>') + + +# --- 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:] + + # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) + if isinstance(p_value, str) and p_value == '': + continue + # 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}</{xml_name}>') + + + +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'<MetaDataObject[^>]+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 _normalize_elements(defn): + """Convert dict-style elements from --from-object generators to list-style expected by compiler. + Generator format: elements = {"ИмяЭлемента": {"element": "input", "path": "..."}, ...} + Compiler format: elements = [{"input": "ИмяЭлемента", "path": "..."}, ...] + Also handles nested 'elements' in groups and 'columns' in tables recursively. + """ + def convert_elements(els): + if isinstance(els, list): + # Already list format — but may have nested dicts inside groups + result = [] + for el in els: + if isinstance(el, dict): + el = dict(el) # copy + if 'elements' in el and isinstance(el['elements'], dict): + el['elements'] = convert_elements(el['elements']) + if 'columns' in el and isinstance(el['columns'], dict): + el['columns'] = convert_columns(el['columns']) + result.append(el) + return result + if isinstance(els, dict): + result = [] + for name, props in els.items(): + if not isinstance(props, dict): + continue + new_el = {} + el_type = props.get('element', 'input') + # Map element type to the key name used in JSON DSL + type_map = { + 'input': 'input', 'check': 'check', 'labelField': 'labelField', + 'table': 'table', 'group': 'group', 'pages': 'pages', + 'page': 'page', 'label': 'label', 'button': 'button', + 'checkBox': 'check', 'radioButton': 'radioButton', + 'pictureField': 'pictureField', + } + mapped_type = type_map.get(el_type, el_type) + new_el[mapped_type] = name + for k, v in props.items(): + if k == 'element': + continue + if k == 'elements' and isinstance(v, dict): + new_el['elements'] = convert_elements(v) + elif k == 'columns' and isinstance(v, dict): + new_el['columns'] = convert_columns(v) + elif k == 'groupType': + # groupType → group property in DSL + new_el['group'] = v + elif k == 'showTitle': + new_el['showTitle'] = v + elif k == 'representation': + new_el['representation'] = v + elif k == 'autoCommandBar': + new_el['autoCommandBar'] = v + elif k == 'commandBarLocation': + new_el['commandBarLocation'] = v + else: + new_el[k] = v + result.append(new_el) + return result + return els + + def convert_columns(cols): + if isinstance(cols, list): + return cols + if isinstance(cols, dict): + result = [] + for name, props in cols.items(): + if not isinstance(props, dict): + continue + new_col = {} + el_type = props.get('element', 'input') + type_map = { + 'input': 'input', 'check': 'check', 'labelField': 'labelField', + 'checkBox': 'check', + } + mapped_type = type_map.get(el_type, el_type) + new_col[mapped_type] = name + for k, v in props.items(): + if k == 'element': + continue + new_col[k] = v + result.append(new_col) + return result + return cols + + if 'elements' in defn: + defn['elements'] = convert_elements(defn['elements']) + return defn + + +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', # ФормаГруппы + '\u0424\u043e\u0440\u043c\u0430\u0417\u0430\u043f\u0438\u0441\u0438': 'Record', # ФормаЗаписи + '\u0424\u043e\u0440\u043c\u0430\u0421\u0447\u0435\u0442\u0430': 'Item', # ФормаСчета + '\u0424\u043e\u0440\u043c\u0430\u0423\u0437\u043b\u0430': 'Item', # ФормаУзла + } + + # 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'], + 'InformationRegister': ['Record', 'List'], + 'AccumulationRegister': ['List'], + 'ChartOfCharacteristicTypes': ['Item', 'Folder', 'List', 'Choice'], + 'ExchangePlan': ['Item', 'List', 'Choice'], + 'ChartOfAccounts': ['Item', 'Folder', 'List', 'Choice'], + } + if meta['Type'] not in supported: + print(f"Object type '{meta['Type']}' not supported. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts.", 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) + + dsl_dispatch = { + 'Document': generate_document_dsl, + 'Catalog': generate_catalog_dsl, + 'InformationRegister': generate_information_register_dsl, + 'AccumulationRegister': generate_accumulation_register_dsl, + 'ChartOfCharacteristicTypes': generate_chart_of_characteristic_types_dsl, + 'ExchangePlan': generate_exchange_plan_dsl, + 'ChartOfAccounts': generate_chart_of_accounts_dsl, + } + dsl = dsl_dispatch[meta['Type']](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 + # Convert dict-style elements (from generators) to list-style (expected by compiler) + defn = _normalize_elements(defn) + 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) + global QUERY_BASE_DIR + QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path)) + + # --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction --- + def _normalize_synonyms(el): + if not isinstance(el, dict): + return + # Companion-панели (объект/массив-значение) → commandBar/contextMenu + normalize_panel_synonyms(el) + # Тип-синонимы: commandBar/autoCommandBar → элемент-тип ТОЛЬКО при строковом значении + synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar', 'extTooltip': 'extendedTooltip'} + for src, dst in synonyms.items(): + if src in el and dst not in el: + if src in STR_ONLY_TYPE_SYNONYMS and not isinstance(el[src], str): + continue + el[dst] = el.pop(src) + # Рекурсия в детей панелей (commandBar/contextMenu) + for pk in ('commandBar', 'contextMenu'): + pv = el.get(pk) + kids = pv if isinstance(pv, list) else (pv.get('children') if isinstance(pv, dict) else None) + if isinstance(kids, list): + for child in kids: + _normalize_synonyms(child) + if isinstance(el.get('children'), list): + for child in el['children']: + _normalize_synonyms(child) + if isinstance(el.get('columns'), list): + for child in el['columns']: + _normalize_synonyms(child) + + def _has_cmd_bar_recursive(el): + if not isinstance(el, dict): + return False + if el.get('cmdBar') is not None: + return True + if isinstance(el.get('children'), list): + for child in el['children']: + if _has_cmd_bar_recursive(child): + return True + if isinstance(el.get('columns'), list): + for child in el['columns']: + if _has_cmd_bar_recursive(child): + return True + return False + + def _apply_dlist_table_heuristic(el, list_name, has_main_table): + if not isinstance(el, dict): + return + if el.get('table') is not None and str(el.get('path', '')) == list_name: + # Маркер дин-список-таблицы → emit_table эмитит блок свойств + el['_dynList'] = True + if 'tableAutofill' not in el: + el['tableAutofill'] = False + if 'commandBarLocation' not in el: + el['commandBarLocation'] = 'None' + # RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ. + # Декомпилятор опускает при rpdp == smart-default; реальное отсутствие → ""-маркер (не + # перезатирается). Гейт has_main_table снят: дин-список без mainTable тоже несёт RowPictureDataPath. + if 'rowPictureDataPath' not in el: + el['rowPictureDataPath'] = f'{list_name}.DefaultPicture' + if isinstance(el.get('children'), list): + for child in el['children']: + _apply_dlist_table_heuristic(child, list_name, has_main_table) + + def _is_object_like_type(t): + if not t: + return False + if t == 'DynamicList' or t == 'ConstantsSet': + return True + object_suffixes = ( + 'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject', + 'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject', + 'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject', + 'ChartOfCalculationTypesObject', 'ExchangePlanObject', + ) + record_set_prefixes = ( + 'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet', + 'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet', + 'InformationRegisterRecordManager', + ) + for s in object_suffixes: + if t.startswith(s + '.'): + return True + for s in record_set_prefixes: + if t.startswith(s + '.'): + return True + return False + + # 1b.1: Normalize synonyms recursively + if isinstance(defn.get('elements'), list): + for el in defn['elements']: + _normalize_synonyms(el) + + # 1b.2: Extract autoCmdBar element from defn['elements'] + main_acb_def = None + if isinstance(defn.get('elements'), list): + auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None] + if len(auto_bars) > 1: + print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr) + sys.exit(1) + if len(auto_bars) == 1: + main_acb_def = auto_bars[0] + defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def] + + # 1b.3: Infer main attribute + if isinstance(defn.get('attributes'), list): + has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict)) + if not has_explicit_main: + candidates = [] + for a in defn['attributes']: + if not isinstance(a, dict): + continue + if 'main' in a and a.get('main') is False: + continue + if _is_object_like_type(str(a.get('type', ''))): + candidates.append(a) + if len(candidates) == 1: + candidates[0]['main'] = True + print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})") + elif len(candidates) > 1: + names = ', '.join(c.get('name', '') for c in candidates) + print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly") + + # 1b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main) + if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list): + for attr in defn['attributes']: + if not isinstance(attr, dict) or str(attr.get('type', '')) != 'DynamicList': + continue + settings = attr.get('settings') or {} + has_mt = bool(isinstance(settings, dict) and settings.get('mainTable')) + for el in defn['elements']: + _apply_dlist_table_heuristic(el, attr.get('name', ''), has_mt) + + # 1b.5: Compute main AutoCommandBar Autofill (B3) + def _compute_main_acb_autofill(): + if main_acb_def is not None: + if 'autofill' in main_acb_def: + return bool(main_acb_def.get('autofill')) + return True + if isinstance(defn.get('elements'), list): + for el in defn['elements']: + if _has_cmd_bar_recursive(el): + return False + return True + + # --- 2. Main compilation --- + _next_id = 0 + _seen_element_names.clear() # пул имён элементов (на случай повторного вызова в одном процессе) + lines = [] + + lines.append('<?xml version="1.0" encoding="UTF-8"?>') + lines.append(f'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">') + + # 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', form_title) + + # Properties (skip 'title' — handled above) + # When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this; + # otherwise platform appends synonym → "Title: Synonym" double-titles). + props_src = defn.get('properties') or {} + props_clone = OrderedDict() + if form_title and 'autoTitle' not in props_src: + props_clone['autoTitle'] = False + for k, v in props_src.items(): + if k != 'title': + props_clone[k] = v + emit_properties(lines, props_clone, '\t') + + # CommandSet (excluded commands) + if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0: + lines.append('\t<CommandSet>') + for cmd in defn['excludedCommands']: + lines.append(f'\t\t<ExcludedCommand>{cmd}</ExcludedCommand>') + lines.append('\t</CommandSet>') + + # MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок + # (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value). + if defn.get('mobileCommandBarContent') and len(defn['mobileCommandBarContent']) > 0: + lines.append('\t<MobileDeviceCommandBarContent>') + for nm in defn['mobileCommandBarContent']: + lines.append('\t\t<xr:Item>') + lines.append('\t\t\t<xr:Presentation/>') + lines.append('\t\t\t<xr:CheckState>0</xr:CheckState>') + lines.append(f'\t\t\t<xr:Value xsi:type="xs:string">{esc_xml(str(nm))}</xr:Value>') + lines.append('\t\t</xr:Item>') + lines.append('\t</MobileDeviceCommandBarContent>') + + # AutoCommandBar (always present, id=-1) + acb_autofill = _compute_main_acb_autofill() + acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c' + acb_halign = None + if main_acb_def is not None: + v = main_acb_def.get('autoCmdBar') + if v: + acb_name = str(v) + if main_acb_def.get('name'): + acb_name = str(main_acb_def['name']) + if main_acb_def.get('horizontalAlign'): + acb_halign = str(main_acb_def['horizontalAlign']) + has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0) + has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children + if has_inner: + lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">') + if acb_halign: + lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>') + if not acb_autofill: + lines.append('\t\t<Autofill>false</Autofill>') + if has_acb_children: + lines.append('\t\t<ChildItems>') + for child in main_acb_def['children']: + emit_element(lines, child, '\t\t\t', in_cmd_bar=True) + lines.append('\t\t</ChildItems>') + lines.append('\t</AutoCommandBar>') + else: + lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>') + + # 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<Events>') + for evt_name, evt_handler in defn['events'].items(): + lines.append(f'\t\t<Event name="{evt_name}">{evt_handler}</Event>') + lines.append('\t</Events>') + + # ChildItems (elements) + if defn.get('elements') and len(defn['elements']) > 0: + lines.append('\t<ChildItems>') + for el in defn['elements']: + emit_element(lines, el, '\t\t') + lines.append('\t</ChildItems>') + + # Attributes + emit_attributes(lines, defn.get('attributes'), '\t', conditional_appearance=defn.get('conditionalAppearance')) + + # Parameters + emit_parameters(lines, defn.get('parameters'), '\t') + + # Commands + emit_commands(lines, defn.get('commands'), '\t') + + # CommandInterface (командный интерфейс формы — последний дочерний Form) + emit_command_interface(lines, defn.get('commandInterface'), '\t') + + # Close + lines.append('</Form>') + + # --- 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>{form_name}</Form>' not in raw_text: + # Insert before </ChildObjects> + if '</ChildObjects>' in raw_text: + insert_line = f'\t\t\t<Form>{form_name}</Form>\n' + raw_text = raw_text.replace('</ChildObjects>', insert_line + '\t\t</ChildObjects>', 1) + elif '<ChildObjects/>' in raw_text: + replacement = f'<ChildObjects>\n\t\t\t<Form>{form_name}</Form>\n\t\t</ChildObjects>' + raw_text = raw_text.replace('<ChildObjects/>', replacement, 1) + + write_utf8_bom(object_xml_path, raw_text) + print(f" Registered: <Form>{form_name}</Form> 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() diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index d77a43eb..f5a5d0c6 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,2628 +1,2637 @@ -# form-decompile v0.110 — Decompile 1C managed Form.xml to JSON DSL (draft) -# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. -param( - [Parameter(Mandatory)] - [Alias('Path')] - [string]$FormPath, - - [string]$OutputPath -) - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# --- 0. Resolve and validate input --- -if (-not (Test-Path $FormPath)) { - Write-Error "Form not found: $FormPath" - exit 1 -} -$FormPath = (Resolve-Path $FormPath).Path - -$xmlDoc = New-Object System.Xml.XmlDocument -$xmlDoc.PreserveWhitespace = $false -$xmlDoc.Load($FormPath) -$root = $xmlDoc.DocumentElement - -# Ring 2: not a managed Form -if ($root.LocalName -ne 'Form') { - [Console]::Error.WriteLine("form-decompile: корневой элемент <$($root.LocalName)> не <Form> — это не управляемая форма.") - exit 2 -} - -# --- 1. Namespaces --- -$NS_LF = "http://v8.1c.ru/8.3/xcf/logform" -$NS_V8 = "http://v8.1c.ru/8.1/data/core" -$NS_XR = "http://v8.1c.ru/8.3/xcf/readable" -$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" - -$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings" -$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema" -$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core" -$NS_V8UI = "http://v8.1c.ru/8.1/data/ui" -$NS_APP = "http://v8.1c.ru/8.2/managed-application/core" - -$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) -$ns.AddNamespace("lf", $NS_LF) -$ns.AddNamespace("v8", $NS_V8) -$ns.AddNamespace("xr", $NS_XR) -$ns.AddNamespace("xsi", $NS_XSI) -$ns.AddNamespace("dcsset", $NS_DCSSET) -$ns.AddNamespace("dcssch", $NS_DCSSCH) -$ns.AddNamespace("dcscor", $NS_DCSCOR) -$ns.AddNamespace("v8ui", $NS_V8UI) -$ns.AddNamespace("app", $NS_APP) - -# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). -# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе, -# компилятор регенерит тот же скелет → чистый раундтрип. -$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' -$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' -$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' -$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' - -# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) --- -$script:outputDir = $null -$script:outputBasename = $null -if ($OutputPath) { - $od = Split-Path -Parent $OutputPath - if (-not $od) { $od = (Get-Location).Path } - $script:outputDir = $od - $script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath) -} -$script:queryFilesAccumulator = @() -$script:queryFileNamesUsed = @{} - -# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file". -function Maybe-ExternalizeQuery { - param([string]$queryText, [string]$listName) - if (-not $queryText) { return $queryText } - if (-not $script:outputDir) { return $queryText } - $lineCount = ([regex]::Matches($queryText, "`n")).Count + 1 - if ($lineCount -lt 3) { return $queryText } - $safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' } - $prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' } - $fileName = "$prefix$safe.sql" - $suffix = 1 - while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" } - $script:queryFileNamesUsed[$fileName] = $true - $script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText } - return "@$fileName" -} -function Save-QueryFiles { - if ($script:queryFilesAccumulator.Count -eq 0) { return } - if (-not $script:outputDir) { return } - $enc = New-Object System.Text.UTF8Encoding($false) - foreach ($qf in $script:queryFilesAccumulator) { - [System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc) - } - [Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)") -} - -# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/ -# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false: -# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness -# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже). -function Test-ListSettingsHasContent { - param($lsNode) - if (-not $lsNode) { return $false } - foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) { - $cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns) - if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true } - } - return $false -} - -# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance → -# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма == -# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы -# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора. -function Get-ListSettingsShape { - param($lsNode) - if (-not $lsNode) { return $null } - $shape = [ordered]@{} - foreach ($child in $lsNode.ChildNodes) { - if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - $tag = $child.LocalName - if ($tag -in @('filter','order','conditionalAppearance')) { - $hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns) - $hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns) - $shape[$tag] = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" - } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } - elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } - else { return $null } # item/dataParameters/itemsUserSettingPresentation/… → канон-fallback - } - # Полный каноничный скелет → опускаем (компилятор регенерит) - if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and ` - $shape['conditionalAppearance'] -eq 'vu' -and $shape['itemsViewMode'] -eq $true -and $shape['itemsUserSettingID'] -eq $true) { return $null } - return $shape -} - -# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) --- -function Fail-Ring3 { - param([string]$kind, [string]$loc) - [Console]::Error.WriteLine("form-decompile: декомпиляция пока не поддерживает $kind (path: $loc)") - [Console]::Error.WriteLine("Для точечной работы с этой формой используй /form-edit.") - exit 3 -} -# ConditionalAppearance со scope (привязка к области) пока не воспроизводим — fail-ring3 только в этом случае. -foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*[local-name()='item']/*[local-name()='scope'][node()]")) { Fail-Ring3 -kind "ConditionalAppearance со scope" -loc "form/ConditionalAppearance/item/scope" } -# Реквизит с design-time конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… — -# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие -# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча. -foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) { - $st = $s.GetAttribute("type", $NS_XSI) - if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') { - Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings" - } - # Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с - # префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые - # дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны. - elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) { - Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings" - } -} - -# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) --- -function Convert-StringToJsonLiteral { - param([string]$s) - if ($null -eq $s) { return 'null' } - $sb = New-Object System.Text.StringBuilder - [void]$sb.Append('"') - foreach ($ch in $s.ToCharArray()) { - $code = [int]$ch - if ($code -eq 0x22) { [void]$sb.Append('\"') } - elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } - elseif ($code -eq 0x08) { [void]$sb.Append('\b') } - elseif ($code -eq 0x09) { [void]$sb.Append('\t') } - elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } - elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } - elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } - elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } - else { [void]$sb.Append($ch) } - } - [void]$sb.Append('"') - return $sb.ToString() -} -function Try-InlineJson { - param($obj) - if ($null -eq $obj) { return 'null' } - if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } - if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } - if ($obj -is [int] -or $obj -is [long]) { return "$obj" } - if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { - return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) - } - if ($obj -is [System.Collections.IDictionary]) { - if ($obj.Count -eq 0) { return '{}' } - $parts = @() - foreach ($k in $obj.Keys) { - $v = Try-InlineJson $obj[$k] - if ($null -eq $v) { return $null } - $parts += "$(Convert-StringToJsonLiteral "$k"): $v" - } - return '{ ' + ($parts -join ', ') + ' }' - } - if ($obj -is [array] -or $obj -is [System.Collections.IList]) { - $items = @($obj) - if ($items.Count -eq 0) { return '[]' } - $parts = @() - foreach ($it in $items) { - $v = Try-InlineJson $it - if ($null -eq $v) { return $null } - $parts += $v - } - return '[' + ($parts -join ', ') + ']' - } - return $null -} -function ConvertTo-CompactJson { - param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120) - $indent = $indentUnit * $depth - $childIndent = $indentUnit * ($depth + 1) - if ($null -eq $obj) { return 'null' } - if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } - if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } - if ($obj -is [int] -or $obj -is [long]) { return "$obj" } - if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { - return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) - } - $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) - if ($isContainer) { - $inlineAttempt = Try-InlineJson $obj - if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt } - } - if ($obj -is [System.Collections.IDictionary]) { - $keys = @($obj.Keys) - if ($keys.Count -eq 0) { return '{}' } - $parts = @() - foreach ($k in $keys) { - $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit - $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" - } - return "{`n" + ($parts -join ",`n") + "`n$indent}" - } - if ($obj -is [array] -or $obj -is [System.Collections.IList]) { - $items = @($obj) - if ($items.Count -eq 0) { return '[]' } - $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) - return "[`n" + ($parts -join ",`n") + "`n$indent]" - } - return (Convert-StringToJsonLiteral "$obj") -} - -# --- 2. Helpers --- - -# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей. -# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы -# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions). -$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar') - -# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...} -function Get-LangText { - param($node) - if ($null -eq $node) { return $null } - $items = @($node.SelectNodes("v8:item", $ns)) - if ($items.Count -eq 0) { return $null } - $map = [ordered]@{} - foreach ($it in $items) { - $lang = $it.SelectSingleNode("v8:lang", $ns) - $content = $it.SelectSingleNode("v8:content", $ns) - if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } - } - if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] } - return $map -} - -# Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает -# <v8:content> </v8:content> → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой -# Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue). -# Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель -# «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст. -function Get-LangTextWS { - param($node) - $t = Get-LangText $node - if ($null -eq $t) { return $null } - if ($t -is [string]) { - if ($t -eq '' -and $node.SelectSingleNode("v8:item/v8:content", $ns)) { return ' ' } - return $t - } - foreach ($it in @($node.SelectNodes("v8:item", $ns))) { - $lang = $it.SelectSingleNode("v8:lang", $ns) - $content = $it.SelectSingleNode("v8:content", $ns) - if ($lang -and $content -and $t.Contains($lang.InnerText) -and $t[$lang.InnerText] -eq '') { - $t[$lang.InnerText] = ' ' - } - } - return $t -} - -# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile. -# Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам. -function Title-FromName { - param([string]$name) - if (-not $name) { return '' } - $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') - $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') - $parts = $s -split ' ' - if ($parts.Count -eq 0) { return $s } - $out = New-Object System.Collections.ArrayList - [void]$out.Add($parts[0]) - for ($i = 1; $i -lt $parts.Count; $i++) { - $p = $parts[$i] - if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) } - else { [void]$out.Add($p.ToLower()) } - } - return ($out -join ' ') -} - -# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!). -$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' -function Test-HasRealMarkup { - param($text) - if ($null -eq $text) { return $false } - $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") } - foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } - return $false -} -# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted -# совпал с атрибутом; иначе явный {text, formatted}. -function Get-MLFormattedValue { - param($titleNode) - if (-not $titleNode) { return $null } - # Get-LangTextWS восстанавливает значимый пробел в <v8:content> </v8:content> (одиночный и - # мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "". - $text = Get-LangTextWS $titleNode - if ($null -eq $text) { return $null } - $fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true') - if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text } - $o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o -} - -# Прочитать дочерний скаляр (по local-name, без namespace) -function Get-Child { - param($node, [string]$name) - $c = $node.SelectSingleNode("*[local-name()='$name']") - if ($c) { return $c.InnerText } else { return $null } -} -function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") } -function To-Bool { param([string]$v) return ($v -eq 'true') } - -# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка). -# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type. -function Convert-TypedValue { - param([string]$raw, [string]$xsiType) - switch -regex ($xsiType) { - 'decimal$' { - if ($raw -match '^-?\d+$') { return [int]$raw } - return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture) - } - 'boolean$' { return ($raw -eq 'true') } - default { return $raw } - } -} - -# ===================================================================== -# Захват настроек компоновщика динамического списка (ListSettings): -# filter / order / conditionalAppearance. Логика портирована из навыка -# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance -# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd -# заменён на запись в stderr + пропуск элемента (form-decompile — draft, -# скрипт не падает на непокрытых конструкциях). -# ===================================================================== - -# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text. -function Get-Text { - param($node, [string]$xpath) - if (-not $node) { return $null } - if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText } - $n = $node.SelectSingleNode($xpath, $ns) - if ($n) { return $n.InnerText } else { return $null } -} - -# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash. -# Алиас на уже существующий Get-LangText (тот же контракт). -function Get-MLText { param($node) return (Get-LangText $node) } - -# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string. -# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText. -function Get-PresText { - param($node) - if (-not $node) { return $null } - $ml = Get-MLText $node - if ($null -ne $ml) { return $ml } - if ($node.InnerText) { return $node.InnerText } - return $null -} - -# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo") -function Get-LocalXsiType { - param($node) - if (-not $node) { return $null } - $t = $node.GetAttribute("type", $NS_XSI) - if ($t -match ':(.+)$') { return $matches[1] } - return $t -} - -# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile). -function Get-FontValue { - param($valNode) - $f = [ordered]@{ '@type' = 'Font' } - foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $a = $valNode.Attributes[$attrName] - if ($null -ne $a) { $f[$attrName] = $a.Value } - } - return $f -} - -# Линия (граница) оформления → объект {@type:Line, width, gap, style}. -function Get-LineValue { - param($valNode) - $obj = [ordered]@{ '@type' = 'Line' } - $w = $valNode.GetAttribute("width") - $g = $valNode.GetAttribute("gap") - if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } - if ($g -ne '') { $obj['gap'] = ($g -eq 'true') } - $styleNode = $valNode.SelectSingleNode("v8ui:style", $ns) - if ($styleNode) { $obj['style'] = $styleNode.InnerText } - return $obj -} - -# Прочитать <dcscor:value> в JSON-значение: Font/Line/Field/multilang/raw text. -function Read-AppearanceValueNode { - param($valNode) - if (-not $valNode) { return $null } - $vt = Get-LocalXsiType $valNode - if ($vt -eq 'LocalStringType') { - # НЕ схлопываем одноязычный в строку: значение параметра оформления различает - # xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы - # одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType. - $map = [ordered]@{} - foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) { - $lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns) - if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } - } - return $map - } - if ($vt -eq 'Font') { return (Get-FontValue $valNode) } - if ($vt -eq 'Line') { return (Get-LineValue $valNode) } - # dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь} - if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } } - return $valNode.InnerText -} - -# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd). -$script:filterOpMap = @{ - 'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>='; - 'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn'; - 'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy'; - 'Contains'='contains'; 'NotContains'='notContains'; - 'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith'; - 'Like'='like'; 'NotLike'='notLike'; - 'Filled'='filled'; 'NotFilled'='notFilled' -} - -# Render filter value node → shorthand-acceptable scalar string -function Get-FilterValue { - param($valNode) - if (-not $valNode) { return '_' } - $nil = $valNode.GetAttribute("nil", $NS_XSI) - if ($nil -eq 'true') { return '_' } - $vType = Get-LocalXsiType $valNode - if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText } - if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) } - $txt = $valNode.InnerText - if (-not $txt) { return '_' } - return $txt -} - -# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field). -function Get-FilterValueWithType { - param($valNode) - if (-not $valNode) { return @{ value = '_'; type = $null } } - $rawType = $valNode.GetAttribute("type", $NS_XSI) - $nil = $valNode.GetAttribute("nil", $NS_XSI) - if ($nil -eq 'true') { return @{ value = '_'; type = $null } } - $vType = Get-LocalXsiType $valNode - if ($vType -eq 'LocalStringType') { - return @{ value = (Get-MLText $valNode); type = $rawType } - } - # Стандартная дата начала/окончания. Формы (от самой компактной): - # SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт - # даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType; - # SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал. - if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') { - $variantN = $valNode.SelectSingleNode("v8:variant", $ns) - $dateN = $valNode.SelectSingleNode("v8:date", $ns) - $variantStr = if ($variantN) { $variantN.InnerText } else { '' } - if ($dateN) { - if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') { - return @{ value = $dateN.InnerText; type = $null } # голая дата = SBD Custom шорткат - } - return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType } - } - return @{ value = $variantStr; type = $rawType } # именованный вариант - } - $txt = $valNode.InnerText - if (-not $txt) { return @{ value = '_'; type = $rawType } } - if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } } - if ($vType -eq 'decimal') { - if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } } - return @{ value = [double]$txt; type = $rawType } - } - return @{ value = $txt; type = $rawType } -} - -# Convert filter item node → shorthand string или object form (рекурсивно для групп). -function Build-FilterItem { - param($itemNode, [string]$loc) - $xtype = Get-LocalXsiType $itemNode - if ($xtype -eq 'FilterItemGroup') { - $gt = Get-Text $itemNode "dcsset:groupType" - $groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } } - $items = @() - foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-FilterItem -itemNode $c -loc "$loc/item") - if ($null -ne $bi) { $items += $bi } - } - $gObj = [ordered]@{ group = $groupName; items = $items } - $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) - if ($gPresNode) { - $gPres = Get-MLText $gPresNode - if (-not $gPres) { $gPres = $gPresNode.InnerText } - if ($gPres) { $gObj['presentation'] = $gPres } - } - $gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) - if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText } - $gUSID = Get-Text $itemNode "dcsset:userSettingID" - if ($gUSID) { $gObj['userSettingID'] = 'auto' } - $gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($gUSPN) { - $gUSP = Get-PresText $gUSPN - if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP } - } - return $gObj - } - if ($xtype -ne 'FilterItemComparison') { - [Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)") - return $null - } - $leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns) - $field = if ($leftNode) { $leftNode.InnerText } else { $null } - $ct = Get-Text $itemNode "dcsset:comparisonType" - $op = $script:filterOpMap[$ct] - if (-not $op) { $op = $ct } - - $rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns)) - $value = $null - $valueIsArrayFlag = $false - $valueTypeAttr = $null - if ($rightNodes.Count -eq 1) { - $rn = $rightNodes[0] - if ((Get-LocalXsiType $rn) -eq 'ValueListType') { - $value = @() - $valueIsArrayFlag = $true - } else { - $vt = Get-FilterValueWithType $rn - $value = $vt.value - $autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and ` - ("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') - if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) { - $valueTypeAttr = $vt.type - } - } - } elseif ($rightNodes.Count -gt 1) { - $arr = @() - $rawTypes = @() - foreach ($rn in $rightNodes) { - $arr += (Get-FilterValue $rn) - $rawTypes += $rn.GetAttribute("type", $NS_XSI) - } - $value = $arr - $valueIsArrayFlag = $true - $uniqTypes = @($rawTypes | Sort-Object -Unique) - if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) { - $autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and ` - ($arr.Count -gt 0) -and ` - (@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0) - if (-not $autoDetectsDTV) { - $valueTypeAttr = $uniqTypes[0] - } - } - } - - $use = Get-Text $itemNode "dcsset:use" - $userId = Get-Text $itemNode "dcsset:userSettingID" - $vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) - $viewMode = if ($vmNode) { $vmNode.InnerText } else { $null } - $userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) - $fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) - $fiPres = $null - if ($fiPresNode) { - $fiPres = Get-MLText $fiPresNode - if (-not $fiPres) { $fiPres = $fiPresNode.InnerText } - } - - $flags = @() - if ($use -eq 'false') { $flags += '@off' } - if ($userId) { $flags += '@user' } - if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' } - elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' } - elseif ($viewMode -eq 'Normal') { $flags += '@normal' } - - $noValueOps = @('filled','notFilled') - - if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres) { - $obj = [ordered]@{ field = $field; op = $op } - if ($op -notin $noValueOps -and $null -ne $value) { - if ($valueIsArrayFlag) { - $arrAsList = New-Object System.Collections.ArrayList - foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) } - $obj['value'] = $arrAsList - } else { - $obj['value'] = $value - } - } - if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr } - if ($use -eq 'false') { $obj['use'] = $false } - if ($userId) { $obj['userSettingID'] = 'auto' } - if ($fiPres) { $obj['presentation'] = $fiPres } - if ($viewMode) { $obj['viewMode'] = $viewMode } - if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode } - return $obj - } - - $s = $field - if ($op -in $noValueOps) { - $s += " $op" - } else { - $vDisplay = '_' - if ($null -ne $value) { - if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } } - elseif ("$value" -ne '') { $vDisplay = "$value" } - } - $s += " $op $vDisplay" - } - if ($flags) { $s += ' ' + ($flags -join ' ') } - return $s -} - -# Рекурсивный хелпер одного элемента selection (для conditionalAppearance). -function Build-SelectionItem { - param($item, [string]$loc) - $xt = Get-LocalXsiType $item - if (-not $xt) { - $fName = Get-Text $item "dcsset:field" - if ($fName) { return $fName } - $fieldEl = $item.SelectSingleNode("dcsset:field", $ns) - if ($fieldEl) { return 'Auto' } - } - switch ($xt) { - 'SelectedItemAuto' { - $useV = Get-Text $item "dcsset:use" - if ($useV -eq 'false') { - return [ordered]@{ auto = $true; use = $false } - } - return 'Auto' - } - 'SelectedItemField' { - $fName = Get-Text $item "dcsset:field" - $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) - $title = Get-MLText $titleNode - $vmN = $item.SelectSingleNode("dcsset:viewMode", $ns) - $useV = Get-Text $item "dcsset:use" - $useFalse = ($useV -eq 'false') - if ($title -or $vmN -or $useFalse) { - $obj = [ordered]@{ field = $fName } - if ($useFalse) { $obj['use'] = $false } - if ($title) { $obj['title'] = $title } - if ($vmN) { $obj['viewMode'] = $vmN.InnerText } - return $obj - } - return $fName - } - 'SelectedItemFolder' { - $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) - $folderTitle = Get-MLText $titleNode - $inner = @() - foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-SelectionItem -item $sub -loc "$loc/folder") - if ($null -ne $bi) { $inner += $bi } - } - $entry = [ordered]@{ folder = $folderTitle; items = $inner } - $folderField = Get-Text $item "dcsset:field" - if ($folderField) { $entry['field'] = $folderField } - $plN = $item.SelectSingleNode("dcsset:placement", $ns) - if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') { - $entry['placement'] = $plN.InnerText - } - return $entry - } - default { - [Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)") - return $null - } - } -} - -# Build selection items array (для conditionalAppearance). -function Build-Selection { - param($selNode, [string]$loc) - if (-not $selNode) { return @() } - $out = @() - foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-SelectionItem -item $it -loc $loc) - if ($null -ne $bi) { $out += $bi } - } - return ,$out -} - -# Build order items array. -function Build-Order { - param($ordNode, [string]$loc) - if (-not $ordNode) { return @() } - $out = @() - foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) { - $xt = Get-LocalXsiType $it - switch ($xt) { - 'OrderItemAuto' { $out += 'Auto' } - 'OrderItemField' { - $fn = Get-Text $it "dcsset:field" - $ot = Get-Text $it "dcsset:orderType" - $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) - $useV = Get-Text $it "dcsset:use" - $useFalse = ($useV -eq 'false') - if ($vmN -or $useFalse) { - $obj = [ordered]@{ field = $fn } - if ($useFalse) { $obj['use'] = $false } - if ($ot -eq 'Desc') { $obj['direction'] = 'desc' } - if ($vmN) { $obj['viewMode'] = $vmN.InnerText } - $out += $obj - } else { - if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn } - } - } - default { - [Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)") - } - } - } - return ,$out -} - -# Build appearance dict из <dcsset:appearance> (Line/Font/multilang/nested items). -function Get-SettingsAppearance { - param($appNode) - if (-not $appNode) { return $null } - $dict = [ordered]@{} - foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) { - $pName = Get-Text $it "dcscor:parameter" - $val = $it.SelectSingleNode("dcscor:value", $ns) - if (-not $pName -or -not $val) { continue } - $rawVal = Read-AppearanceValueNode $val - $useV = Get-Text $it "dcscor:use" - $nestedItems = [ordered]@{} - foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) { - $subName = Get-Text $sub "dcscor:parameter" - $subVal = $sub.SelectSingleNode("dcscor:value", $ns) - if (-not $subName) { continue } - $subRaw = Read-AppearanceValueNode $subVal - $subUse = Get-Text $sub "dcscor:use" - $subEntry = [ordered]@{ value = $subRaw } - if ($subUse -eq 'false') { $subEntry['use'] = $false } - $nestedItems[$subName] = $subEntry - } - $valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line') - if ($valIsLine) { - if ($useV -eq 'false') { $rawVal['use'] = $false } - if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems } - $dict[$pName] = $rawVal - } elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) { - $wrap = [ordered]@{ value = $rawVal } - if ($useV -eq 'false') { $wrap['use'] = $false } - if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems } - $dict[$pName] = $wrap - } else { - $dict[$pName] = $rawVal - } - } - return $dict -} - -# Build conditionalAppearance array. -function Build-ConditionalAppearance { - param($caNode, [string]$loc) - if (-not $caNode) { return @() } - $out = @() - $i = 0 - foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) { - $entry = [ordered]@{} - $scopeNode = $it.SelectSingleNode("dcsset:scope", $ns) - if ($scopeNode -and $scopeNode.HasChildNodes) { - [Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)") - } - $selNode = $it.SelectSingleNode("dcsset:selection", $ns) - if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { - $entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection" - } - $filterNode = $it.SelectSingleNode("dcsset:filter", $ns) - if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { - $f = @() - foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter") - if ($null -ne $bi) { $f += $bi } - } - $entry['filter'] = $f - } - $appNode = $it.SelectSingleNode("dcsset:appearance", $ns) - $ap = Get-SettingsAppearance $appNode - if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap } - $presNode = $it.SelectSingleNode("dcsset:presentation", $ns) - if ($presNode) { - $pres = Get-MLText $presNode - if (-not $pres) { $pres = $presNode.InnerText } - if ($pres) { $entry['presentation'] = $pres } - } - $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) - if ($vmN) { $entry['viewMode'] = $vmN.InnerText } - $usid = Get-Text $it "dcsset:userSettingID" - if ($usid) { $entry['userSettingID'] = 'auto' } - $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($uspN) { - $usp = Get-PresText $uspN - if ($usp) { $entry['userSettingPresentation'] = $usp } - } - $useV = Get-Text $it "dcsset:use" - if ($useV -eq 'false') { $entry['use'] = $false } - $useInDontUse = @() - foreach ($ch in $it.ChildNodes) { - if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue } - if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') { - $shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1) - $useInDontUse += $shortName - } - } - if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse } - $out += $entry - $i++ - } - return ,$out -} - -# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора). -# Вызывается один раз для любого элемента. Height тут — высота элемента (<Height>), -# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows. -function Add-Layout { - param($obj, $node) - # Общие свойства элемента (любой тип): default/drag/skip - if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true } - $soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') } - # EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) - $esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') } - $edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') } - $fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm } - # AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault). - # Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики). - $amwNode = Get-Child $node 'AutoMaxWidth' - if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false } - elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true } - elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true } - $mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw } - if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false } - $mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh } - $w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w } - $h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h } - # Stretch: захват фактического значения (true И false — платформа эмитит явное) - $hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') } - $vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') } - $gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha } - $gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva } - $ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha } - # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» - foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') } - } - $fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha } - $hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha } -} - -# TitleLocation у check/radio (зеркало Emit-TitleLocation): -# тега нет → "" (дефолт платформы); значение = умный дефолт → опускаем; иначе пишем. -function Add-TitleLocation { - param($obj, $node, [string]$smartDefault) - $tl = Get-Child $node 'TitleLocation' - if ($null -eq $tl) { $obj['titleLocation'] = '' } - elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() } -} - -# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика } -# в порядке документа. Имена обработчиков всегда явные (как у событий формы) — -# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим. -function Get-Events { - param($node, [string]$elName) - $ev = $node.SelectSingleNode("lf:Events", $ns) - if (-not $ev) { return $null } - $events = [ordered]@{} - foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) { - $events[$e.GetAttribute("name")] = $e.InnerText - } - if ($events.Count -eq 0) { return $null } - return $events -} - -# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use). -# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }. -# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует. -function Decompile-XrFlag { - param($node, [string]$tag) - $el = $node.SelectSingleNode("*[local-name()='$tag']") - if (-not $el) { return $null } - $commonNode = $el.SelectSingleNode("*[local-name()='Common']") - $common = ($commonNode -and $commonNode.InnerText -eq 'true') - $valNodes = @($el.SelectNodes("*[local-name()='Value']")) - if ($valNodes.Count -eq 0) { return $common } - $roles = [ordered]@{} - foreach ($v in $valNodes) { - $rn = $v.GetAttribute("name") - if ($rn -match '^Role\.') { $rn = $rn.Substring(5) } - $roles[$rn] = ($v.InnerText -eq 'true') - } - $o = [ordered]@{} - $o['common'] = $common - $o['roles'] = $roles - return $o -} - -# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel, -# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки). -# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index + -# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible). -# Голый элемент (только команда, Type=Auto) → строковый shorthand. -function Decompile-CommandInterface { - $ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns) - if (-not $ciNode) { return $null } - $ci = [ordered]@{} - foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) { - $pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns) - if (-not $pn) { continue } - $items = New-Object System.Collections.ArrayList - foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) { - $o = [ordered]@{} - $cmd = Get-Child $it 'Command' - $o['command'] = "$cmd" - $ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty } - $at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at } - $cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg } - $idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx } - $dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') } - $vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis } - # Голый элемент (только command) → строка-shorthand; иначе объект - if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) } - } - if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) } - } - if ($ci.Count -gt 0) { return $ci } - return $null -} - -# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть). -function Decompile-FunctionalOptions { - param($node) - $foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns) - if (-not $foNode) { return $null } - $opts = New-Object System.Collections.ArrayList - foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) { - $t = $it.InnerText.Trim() -replace '^FunctionalOption\.', '' - [void]$opts.Add($t) - } - if ($opts.Count -gt 0) { return ,@($opts) } - return $null -} - -# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions. -function Decompile-AttrColumn { - param($c) - $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") - $cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty } - $ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangText $ctNode; if ($null -ne $t) { $co['title'] = $t } } - $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } - # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита (bool | {common,roles}) - $cv = Decompile-XrFlag $c 'View'; if ($null -ne $cv) { $co['view'] = $cv } - $ce = Decompile-XrFlag $c 'Edit'; if ($null -ne $ce) { $co['edit'] = $ce } - return $co -} - -# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash -# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). -# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false. -# Источник: <xr:Ref> (именованная/стилевая) ИЛИ <xr:Abs> (встроенная → префикс "abs:"). -# Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект -# {src, loadTransparent?, transparentPixel?}. -function Get-PictureRef { - param($node, [string]$picTag) - $ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns) - $abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns) - if (-not $ref -and -not $abs) { return $null } - $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } - $lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns) - $ltTrue = ($lt -and $lt.InnerText -eq 'true') - $tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns) - if (-not $ltTrue -and -not $tpx) { return $src } - $o = [ordered]@{ src = $src } - if ($ltTrue) { $o['loadTransparent'] = $true } - if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } - return $o -} - -# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values): -# фиксируем только отклонение false. Источник <xr:Ref> или <xr:Abs> (→ "abs:"). При наличии -# <xr:TransparentPixel> → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture -# + отдельный loadTransparent:false. -function Set-CommandPicture { - param($obj, $node) - $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) - $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) - if (-not $ref -and -not $abs) { return } - $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } - $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns) - $ltFalse = ($lt -and $lt.InnerText -eq 'false') - $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) - if ($tpx) { - $o = [ordered]@{ src = $src } - if ($ltFalse) { $o['loadTransparent'] = $false } - $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } - $obj['picture'] = $o - } else { - $obj['picture'] = $src - if ($ltFalse) { $obj['loadTransparent'] = $false } - } -} - -# Шрифт <Font ...> → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты. -function Build-FontValue { - param($f) - $present = @() - foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - if ($f.HasAttribute($a)) { $present += $a } - } - # Чистый style-ref (ref + kind=StyleItem) → строка-шорткат - if ($present.Count -eq 2 -and ($present -contains 'ref') -and $f.GetAttribute('kind') -eq 'StyleItem') { - return $f.GetAttribute('ref') - } - $o = [ordered]@{} - foreach ($k in $present) { - $v = $f.GetAttribute($k) - if ($k -in @('height','scale') -and $v -match '^-?\d+$') { $o[$k] = [int]$v } - elseif ($k -in @('bold','italic','underline','strikeout')) { $o[$k] = ($v -eq 'true') } - else { $o[$k] = $v } - } - return $o -} - -# Граница <Border> → строка-ref (из стиля) или объект {width, style}. -function Build-BorderValue { - param($b) - if ($b.HasAttribute('ref')) { return $b.GetAttribute('ref') } - $o = [ordered]@{} - if ($b.HasAttribute('width')) { $w = $b.GetAttribute('width'); $o['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } - $st = $b.SelectSingleNode("v8ui:style", $ns) - if ($st) { $o['style'] = $st.InnerText } - return $o -} - -# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка. -function Add-Appearance { - param($obj, $node) - $colorMap = @{ - 'TitleTextColor'='titleTextColor'; 'TitleBackColor'='titleBackColor' - 'FooterTextColor'='footerTextColor'; 'FooterBackColor'='footerBackColor' - 'TextColor'='textColor'; 'BackColor'='backColor'; 'BorderColor'='borderColor' - } - foreach ($tag in $colorMap.Keys) { - $c = $node.SelectSingleNode("lf:$tag", $ns) - if ($c) { $obj[$colorMap[$tag]] = $c.InnerText } - } - foreach ($pair in @(@('Font','font'), @('TitleFont','titleFont'), @('FooterFont','footerFont'))) { - $f = $node.SelectSingleNode("lf:$($pair[0])", $ns) - if ($f) { $obj[$pair[1]] = (Build-FontValue $f) } - } - $b = $node.SelectSingleNode("lf:Border", $ns) - if ($b) { $obj['border'] = (Build-BorderValue $b) } -} - -function Add-CommonProps { - param($obj, $node, [string]$elName) - Add-Appearance $obj $node - if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true } - if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true } - if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true } - $uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv } - $titleNode = $node.SelectSingleNode("lf:Title", $ns) - if ($titleNode) { - $t = Get-LangTextWS $titleNode # восстановление значимого пробела (whitespace-заголовок) - if ($null -ne $t) { $obj['title'] = $t } - # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) - } - $ttNode = $node.SelectSingleNode("lf:ToolTip", $ns) - if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } } - $ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr } - # Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon) - $hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp } - $fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp } - $ev = Get-Events $node $elName - if ($ev) { $obj['events'] = $ev } - # CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture): - # список отключённых команд редактора. Только <ExcludedCommand>, пустого не бывает. - $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) - if ($csNode) { - $exc = New-Object System.Collections.ArrayList - foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } - if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } - } -} - -# --- 3. Type decompile (inverse of Emit-Type) --- -function Decompile-Type { - param($typeNode) - if (-not $typeNode) { return $null } - $parts = New-Object System.Collections.ArrayList - foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) { - $raw = $vt.InnerText.Trim() - $short = $raw - # break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList). - switch -regex ($raw) { - '^xs:string$' { - $len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) - $al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns) - $fixed = ($al -and $al.InnerText -eq 'Fixed') # Variable = дефолт (опускаем); Fixed — явно - if ($len -and [int]$len.InnerText -gt 0) { - $short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" } - } else { $short = "string" } # Length=0 → всегда Variable (корпус) - break - } - '^xs:decimal$' { - $d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) - $f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) - $sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns) - $dd = if ($d) { $d.InnerText } else { '0' } - $ff = if ($f) { $f.InnerText } else { '0' } - if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" } - break - } - '^xs:boolean$' { $short = "boolean"; break } - '^xs:dateTime$' { - $df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) - $dfv = if ($df) { $df.InnerText } else { 'DateTime' } - switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } } - break - } - '^cfg:(.+)$' { $short = $matches[1]; break } - '^(v8|v8ui):' { - # Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом - # (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост. - $rev = @{ - 'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList' - 'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal' - 'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure' - 'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font' - } - if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw } - break - } - default { $short = $raw } - } - [void]$parts.Add($short) - } - # TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида». - # Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена. - foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) { - $raw = $ts.InnerText.Trim() - $short = $raw -replace '^(v8ui|v8|cfg):', '' - [void]$parts.Add($short) - } - if ($parts.Count -eq 0) { return $null } - if ($parts.Count -eq 1) { return $parts[0] } - return ($parts -join ' | ') -} - -# Schema-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии -# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и -# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter. -function Build-DLInputParameters { - param($ipNode) - $items = New-Object System.Collections.ArrayList - foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) { - $io = [ordered]@{} - $io['parameter'] = Get-Child $it 'parameter' - $useN = $it.SelectSingleNode("dcscor:use", $ns) - if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false } - $valN = $it.SelectSingleNode("dcscor:value", $ns) - if ($valN) { - $vt = $valN.GetAttribute("type", $NS_XSI) - if ($vt -match 'ChoiceParameters$') { - $cps = New-Object System.Collections.ArrayList - foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { - $cpo = [ordered]@{} - $cpo['name'] = Get-Child $cpi 'choiceParameter' - $vals = New-Object System.Collections.ArrayList - foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) { - [void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI)))) - } - $cpo['values'] = @($vals) - [void]$cps.Add($cpo) - } - $io['choiceParameters'] = @($cps) - } elseif ($vt -match 'ChoiceParameterLinks$') { - $cpls = New-Object System.Collections.ArrayList - foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { - $cpo = [ordered]@{} - $cpo['name'] = Get-Child $cpi 'choiceParameter' - $cpo['value'] = Get-Child $cpi 'value' - $md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md } - [void]$cpls.Add($cpo) - } - $io['choiceParameterLinks'] = @($cpls) - } else { - if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt } - } - } - [void]$items.Add($io) - } - return @($items) -} - -# dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного -# значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально, -# в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level). -function Build-FormDataParameters { - param($dpNode) - $entries = @() - foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) { - $pn = Get-Text $it "dcscor:parameter" - $use = Get-Text $it "dcscor:use" - $valNode = $it.SelectSingleNode("dcscor:value", $ns) - $usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns) - $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) - $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) - if ($valNode -or $usidN -or $vmN -or $uspN) { - $obj = [ordered]@{ parameter = $pn } - if ($valNode) { - if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true } - else { - $vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText - if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal } - elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') } - else { $obj['value'] = $vVal } - if ($vType) { $obj['valueType'] = $vType } - } - } - if ($use -eq 'false') { $obj['use'] = $false } - if ($usidN) { $obj['userSettingID'] = 'auto' } - if ($vmN) { $obj['viewMode'] = $vmN.InnerText } - if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } } - $entries += $obj - } else { - $s = $pn; if ($use -eq 'false') { $s += ' @off' } - $entries += $s - } - } - return ,$entries -} - -function Build-DLParameter { - param($pNode) - $name = Get-Child $pNode 'name' - $o = [ordered]@{} - $o['name'] = $name - # title — опускаем, если совпадает с авто-выводом из имени (ru-only) - $titleNode = $pNode.SelectSingleNode("dcssch:title", $ns) - if ($titleNode) { - $t = Get-LangText $titleNode - if ($null -ne $t) { - $auto = Title-FromName -name $name - if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t } - } - } - # valueType - $vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns) - $typeVal = $null - if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } } - # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет <value xsi:nil/> - # не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит → - # явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.) - $vNode = $pNode.SelectSingleNode("dcssch:value", $ns) - if ($vNode -and ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true')) { - $o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI)) - } elseif ($vNode -and ((Get-Child $pNode 'valueListAllowed') -eq 'true')) { - $o['value'] = $null - } - # useRestriction — опускаем true (дефолт), фиксируем false - if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false } - # expression - $expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr } - # availableValues - $avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns)) - if ($avNodes.Count -gt 0) { - $avs = New-Object System.Collections.ArrayList - foreach ($avn in $avNodes) { - $avo = [ordered]@{} - $avv = $avn.SelectSingleNode("dcssch:value", $ns) - if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) } - else { $avo['value'] = $null } - $avp = $avn.SelectSingleNode("dcssch:presentation", $ns) - if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } } - [void]$avs.Add($avo) - } - $o['availableValues'] = @($avs) - } - # valueListAllowed / availableAsField - if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true } - if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false } - # inputParameters - $ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns) - if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } } - # denyIncompleteValues / use - if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true } - $use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use } - - # Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект. - $keys = @($o.Keys) - if ($keys.Count -eq 1) { return $name } - if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) } - return $o -} - -# --- 4. Element dispatch --- -$ELEMENT_KEY = @{ - 'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check'; - 'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField'; - 'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar'; - 'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup'; - 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl'; - 'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc'; - 'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar'; - 'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner'; - 'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart' -} - -# Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value. -$GENERIC_SCALARS = @( - @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } - @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } - @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } - @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } - @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } - @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } - @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } - @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } - @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } - @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } - @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } - @{ Tag='Mask'; Key='mask'; Kind='value' } - @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } - @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } - @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } - # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора) - @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } - @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } - @{ Tag='Output'; Key='output'; Kind='value' } - @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } - @{ Tag='PointerType'; Key='pointerType'; Kind='value' } - @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } - @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } - @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } - @{ Tag='Protection'; Key='protection'; Kind='bool' } - @{ Tag='Edit'; Key='edit'; Kind='bool' } - @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } - @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } - @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } - @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } - @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } - @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } - # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы - @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } - @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } - @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } - # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) - @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } - @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } - # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам - @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } - @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } - @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } - @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } - @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } - @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } - # Корпусный хвост (zеркало компилятора): свёртка группы / форма попапа / авто-добавление / - # выделение отрицательных / нач. позиция списка / высота списка выбора / три состояния / прокрутка - @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } - @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } - @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } - @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } - @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } - @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } - @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } - @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } - # Сочетание клавиш — общее свойство элемента (команда — отдельный путь) - @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } - # Батч простых скаляров (зеркало компилятора; Table HeaderHeight/FooterHeight/CurrentRowUse — отдельно) - @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } - @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } - @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } - @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } - @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } - @{ Tag='Shape'; Key='shape'; Kind='value' } - @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } - # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) - @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } - @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } -) - -# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает. -function Add-GenericScalars { - param($obj, $node) - foreach ($s in $GENERIC_SCALARS) { - if ($obj.Contains($s.Key)) { continue } - $v = Get-Child $node $s.Tag - if ($null -eq $v) { continue } - if ($s.Kind -eq 'bool') { $obj[$s.Key] = ($v -eq 'true') } else { $obj[$s.Key] = $v } - } -} - -function Decompile-Children { - param($parentNode, [string]$childContainer = 'ChildItems') - $container = $parentNode.SelectSingleNode("lf:$childContainer", $ns) - if (-not $container) { return $null } - $list = New-Object System.Collections.ArrayList - foreach ($child in $container.ChildNodes) { - if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - if ($COMPANION_TAGS -contains $child.LocalName) { continue } - $el = Decompile-Element $child - if ($el) { [void]$list.Add($el) } - } - if ($list.Count -eq 0) { return $null } - return ,@($list) -} - -# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом -# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing). -# Дин-список-таблица: компилятор-эвристика додумывает Autofill=false. Выражаем только ОТКЛОНЕНИЕ: -# autofill=false (нет детей) → совпадает с дефолтом → молчим; -# голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта). -function Decompile-CompanionPanel { - param($node, [string]$tag, [bool]$isDynListTable = $false) - $p = $node.SelectSingleNode("lf:$tag", $ns) - if (-not $p) { return $null } - $autofillRaw = Get-Child $p 'Autofill' - $halign = Get-Child $p 'HorizontalAlign' - $kids = Decompile-Children $p - $hasKids = $kids -and @($kids).Count -gt 0 - if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) { - if ($autofillRaw -eq 'false') { return $null } # = дефолт эвристики → молчим - return [ordered]@{ autofill = $true } # голая панель → отклонение - } - if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null } - $o = [ordered]@{} - if ($halign) { $o['horizontalAlign'] = $halign } - if ($autofillRaw -eq 'false') { $o['autofill'] = $false } - elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true } - if ($hasKids) { $o['children'] = $kids } - return $o -} - -# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null. -# У RadioButtonField и InputField. -function Decompile-ChoiceList { - param($node) - $cl = $node.SelectSingleNode("lf:ChoiceList", $ns) - if (-not $cl) { return $null } - $items = New-Object System.Collections.ArrayList - foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) { - $valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns) - $presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns) - $ci = [ordered]@{} - if ($valNode) { - $xsiType = $valNode.GetAttribute("type", $NS_XSI) - $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType - # Системное перечисление (ent:*) / иной не-примитивный, не-DesignTimeRef тип → сохраняем - # valueType (Normalize-ChoiceValue в компиляторе вывела бы xs:string и потеряла тип). - if ($xsiType -and $xsiType -notmatch '^xs:(string|decimal|boolean|dateTime)$' -and $xsiType -ne 'xr:DesignTimeRef') { - $ci['valueType'] = $xsiType - } - } - # Presentation: непустой → текст/мультиязык; пустой <Presentation/> → "" — суппресс-маркер, - # подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения). - if ($presNode) { - $p = Get-LangText $presNode - if ($null -ne $p -and $p -ne '') { $ci['presentation'] = $p } else { $ci['presentation'] = '' } - } - [void]$items.Add($ci) - } - if ($items.Count -gt 0) { return ,@($items) } - return $null -} - -# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо -# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value). -function Convert-ChoiceParamValue { - param($valNode) - $vt = $valNode.GetAttribute("type", $NS_XSI) - if ($vt -match 'FixedArray$') { - $arr = New-Object System.Collections.ArrayList - foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) { - $inner = $it.SelectSingleNode("lf:Value", $ns) - if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) } - } - return ,@($arr) - } - return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt -} - -# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}]. -function Decompile-ChoiceParameters { - param($node) - $cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns) - if (-not $cpn) { return $null } - $items = New-Object System.Collections.ArrayList - foreach ($it in @($cpn.SelectNodes("app:item", $ns))) { - $o = [ordered]@{} - $o['name'] = $it.GetAttribute("name") - $valNode = $it.SelectSingleNode("app:value/lf:Value", $ns) - if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode } - [void]$items.Add($o) - } - if ($items.Count -gt 0) { return ,@($items) } - return $null -} - -# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> → -# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит). -function Decompile-ChoiceParameterLinks { - param($node) - $cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns) - if (-not $cln) { return $null } - $items = New-Object System.Collections.ArrayList - foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) { - $o = [ordered]@{} - $o['name'] = Get-Text $lk "xr:Name" - $o['dataPath'] = Get-Text $lk "xr:DataPath" - $vc = Get-Text $lk "xr:ValueChange" - if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc } - [void]$items.Add($o) - } - if ($items.Count -gt 0) { return ,@($items) } - return $null -} - -# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}. -function Decompile-TypeLink { - param($node) - $tn = $node.SelectSingleNode("lf:TypeLink", $ns) - if (-not $tn) { return $null } - $o = [ordered]@{} - $o['dataPath'] = Get-Text $tn "xr:DataPath" - $li = Get-Text $tn "xr:LinkItem" - if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li } - return $o -} - -# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}). -function Add-FormatProps { - param($obj, $node) - $fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } } - $efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } } -} - -# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation. -# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно). -function Add-AdditionCore { - param($obj, $node, [string]$elName) - $src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText } - Add-CommonProps $obj $node $elName - $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } -} - -# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту -# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем. -function Decompile-TableAdditions { - param($tableNode, [string]$tableName) - $tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' } - $map = [ordered]@{} - foreach ($child in $tableNode.ChildNodes) { - if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } - if (-not $tagToKey.ContainsKey($child.LocalName)) { continue } - $key = $tagToKey[$child.LocalName] - $nm = $child.GetAttribute("name") - $o = [ordered]@{}; $o[$key] = $nm - Add-AdditionCore $o $child $nm - Add-Layout $o $child - $o.Remove($key) # имя авто - if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') } # source=таблица дефолт - if ($o.Count -gt 0) { $map[$key] = $o } - } - if ($map.Count -gt 0) { return $map } - return $null -} - -# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode). -# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост. -function Decompile-SimpleField { - param($obj, $node, [string]$name, [string]$key) - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } -} - -# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField). -function Add-GaugeScalars { - param($obj, $node, $tags) - foreach ($p in $tags) { - $v = Get-Child $node $p - if ($null -eq $v) { continue } - $key = $p.Substring(0,1).ToLower() + $p.Substring(1) - if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v } - } -} - -function Decompile-Element { - param($node) - $tag = $node.LocalName - if (-not $ELEMENT_KEY.ContainsKey($tag)) { - Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag" - } - $key = $ELEMENT_KEY[$tag] - $name = $node.GetAttribute("name") - $obj = [ordered]@{} - - switch ($tag) { - 'UsualGroup' { - # group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем). - # group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление - # не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical' - # — иначе компилятор додумает <Group>Vertical</Group> там, где его нет. - $g = Get-Child $node 'Group' - $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } - if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } - $behavior = Get-Child $node 'Behavior' - if ($behavior) { - $bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' } - if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior } - } - $obj['name'] = $name - Add-CommonProps $obj $node $name - $rep = Get-Child $node 'Representation' - if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } } - $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) - $crt = $node.SelectSingleNode("lf:CollapsedRepresentationTitle", $ns); if ($crt) { $ct = Get-LangText $crt; if ($null -ne $ct -and $ct -ne '') { $obj['collapsedTitle'] = $ct } } - if ((Get-Child $node 'United') -eq 'false') { $obj['united'] = $false } - if ((Get-Child $node 'Collapsed') -eq 'true') { $obj['collapsed'] = $true } - # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы) - Add-FormatProps $obj $node - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'ColumnGroup' { - # columnGroup = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление - # не эмитим). Иначе компилятор додумает <Group>Horizontal</Group> там, где его нет. - $g = Get-Child $node 'Group' - $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'InCell'='inCell' } - if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } - $obj['name'] = $name - Add-CommonProps $obj $node $name - $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) - $sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'InputField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - # MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true») - $mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') } - # PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true») - $pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') } - $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangText $ih; if ($t) { $obj['inputHint'] = $t } } - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangText $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } - foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton','ChoiceListButton')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } - } - # InputField-специфичные bool-скаляры (захват «как есть») - foreach ($p in @('Wrap','OpenButton','ListChoiceMode','ExtendedEditMultipleValues','ChooseType','QuickChoice','AutoChoiceIncomplete')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } - } - # InputField-специфичные value-скаляры (захват «как есть») - foreach ($p in @('ChoiceForm','ChoiceHistoryOnInput','ChoiceFoldersAndItems','FooterDataPath')) { - $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = $v } - } - # MinValue/MaxValue — типизированное (<MinValue xsi:type="xs:decimal">N). Тип кодируем в JSON-тип: - # xs:decimal/int → число (компилятор → xs:decimal), иначе → строка (компилятор → xs:string). - foreach ($p in @('MinValue','MaxValue')) { - $mn = $node.SelectSingleNode("lf:$p", $ns) - if ($mn) { - $xt = $mn.GetAttribute("type", $NS_XSI); $txt = $mn.InnerText - $key = $p.Substring(0,1).ToLower() + $p.Substring(1) - if ($xt -match 'decimal|int') { - if ($txt -match '^-?\d+$') { $obj[$key] = [int]$txt } elseif ($txt -match '^-?\d+\.\d+$') { $obj[$key] = [decimal]$txt } else { $obj[$key] = $txt } - } else { $obj[$key] = $txt } - } - } - # Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор. - # availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type. - $tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) } - $atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } } - $cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr } - $cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp } - if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false } - $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } - Add-FormatProps $obj $node - # Параметры выбора / Связи параметров выбора / Связь по типу - $cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp } - $cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl } - $tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk } - } - 'CheckBoxField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - # CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение - $cbt = Get-Child $node 'CheckBoxType' - if ($null -eq $cbt) { $obj['checkBoxType'] = '' } - elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) } - Add-TitleLocation $obj $node 'Right' - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - Add-FormatProps $obj $node - } - 'RadioButtonField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - Add-TitleLocation $obj $node 'None' - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } - $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } - } - 'LabelDecoration' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } - # title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip) - $tiNode = $node.SelectSingleNode("lf:Title", $ns) - if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } - } - 'LabelField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - # LabelField: тег <Hiperlink> (опечатка платформы), не <Hyperlink> - if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true } - # PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения - $pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') } - $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } - # FooterDataPath / FooterText — общие cell-свойства колонки (как у input), не только input - $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } - $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangText $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } - Add-FormatProps $obj $node - } - 'PictureDecoration' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - # title декорации — единая ML-text форма с formatted (атрибут <Title formatted> у PictureDecoration) - $tiNode = $node.SelectSingleNode("lf:Title", $ns) - if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } - $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangText $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } - $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) - $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) - if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" } # встроенная картинка → префикс abs: - $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true } - # Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности - $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) - if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } - if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } - } - 'PictureField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } - $vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp } - $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangText $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } - } - 'CalendarField' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm } - $scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') } - $wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim } - $him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him } - $smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') } - } - 'Table' { - $obj[$key] = $name - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') } - $cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') } - if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true } - # enableDrag — теперь общий (Add-Layout, фактическое значение) - if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null } - if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false } - if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true } - # Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout) - $htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr } - # Высота шапки/подвала таблицы в строках (pass-through; компилятор эмитит в Emit-Table) - $hh = Get-Child $node 'HeaderHeight'; if ($null -ne $hh) { $obj['headerHeight'] = [int]$hh } - $fh = Get-Child $node 'FooterHeight'; if ($null -ne $fh) { $obj['footerHeight'] = [int]$fh } - # Использование текущей строки (Table-уровень; ≠ command-level CurrentRowUse) — pass-through - $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } - # Запрос обновления дин-списка (всегда PullFromTop в корпусе) — pass-through - $rr = Get-Child $node 'RefreshRequest'; if ($rr) { $obj['refreshRequest'] = $rr } - # CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем - # (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват). - $cbl = Get-Child $node 'CommandBarLocation' - if (Has-Child $node 'UpdateOnDataChange') { - if ($null -eq $cbl) { $obj['commandBarLocation'] = '' } - elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl } - } elseif ($cbl) { $obj['commandBarLocation'] = $cbl } - $ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl } - $vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl } - $scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl } - # --- Общие свойства таблицы (любой тип таблицы, не только динсписок) --- - if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true } - $selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm } - $rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm } - if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false } - if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false } - if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true } - # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill). - $taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') } - if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true } - $soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin } - $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } - $itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv } - # RowsPicture — конвенция ValuesPicture (Ref/Abs + LoadTransparent дефолт false + TransparentPixel) - $rp = Get-PictureRef $node 'RowsPicture'; if ($null -ne $rp) { $obj['rowsPicture'] = $rp } - $rpdp = Get-Child $node 'RowPictureDataPath' - # --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) --- - if (Has-Child $node 'UpdateOnDataChange') { - if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true } - $arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp } - $cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi } - if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true } - if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false } - if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true } - $uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc } - if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false } - # RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture - if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' } - elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp } - $usg = Get-Child $node 'UserSettingsGroup'; if ($usg) { $obj['userSettingsGroup'] = $usg } - } elseif ($rpdp) { $obj['rowPictureDataPath'] = $rpdp } - $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) - if ($csNode) { - $exc = New-Object System.Collections.ArrayList - foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } - if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } - } - $cols = Decompile-Children $node - if ($cols) { $obj['columns'] = $cols } - # Стандартные дополнения уровня таблицы (прямые дети) → карта отклонений additions - $addMap = Decompile-TableAdditions $node $name - if ($addMap) { $obj['additions'] = $addMap } - } - 'Pages' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $pr = Get-Child $node 'PagesRepresentation'; if ($pr) { $obj['pagesRepresentation'] = $pr } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'Page' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $g = Get-Child $node 'Group' - $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } - if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] } - # Картинка страницы (иконка вкладки) — конвенция ValuesPicture (дефолт LoadTransparent=false) - $pp = Get-PictureRef $node 'Picture'; if ($null -ne $pp) { $obj['picture'] = $pp } - $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) - # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы) - Add-FormatProps $obj $node - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'Button' { - $obj[$key] = $name - $cmd = Get-Child $node 'CommandName' - if ($cmd) { - if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] } - elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] } - elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" } - else { $obj['commandName'] = $cmd } - } - $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } - Add-CommonProps $obj $node $name - $type = Get-Child $node 'Type' - if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } } - if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true } - if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true } - Set-CommandPicture $obj $node - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic } - } - 'ButtonGroup' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $cs = Get-Child $node 'CommandSource'; if ($cs) { $obj['commandSource'] = $cs } - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'CommandBar' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - $cs = Get-Child $node 'CommandSource'; if ($cs) { $obj['commandSource'] = $cs } - $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } - if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'Popup' { - $obj[$key] = $name - Add-CommonProps $obj $node $name - Set-CommandPicture $obj $node - $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } - $kids = Decompile-Children $node - if ($kids) { $obj['children'] = $kids } - } - 'SearchStringAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } - 'ViewStatusAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } - 'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } - 'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'HTMLDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'TextDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'FormattedDocumentField' { Decompile-SimpleField $obj $node $name $key } - 'ProgressBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') } - 'TrackBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') } - 'ChartField' { Decompile-SimpleField $obj $node $name $key } - 'GraphicalSchemaField' { Decompile-SimpleField $obj $node $name $key } - 'PlannerField' { Decompile-SimpleField $obj $node $name $key } - 'PeriodField' { Decompile-SimpleField $obj $node $name $key } - 'DendrogramField' { Decompile-SimpleField $obj $node $name $key } - 'GanttChartField' { - Decompile-SimpleField $obj $node $name $key - # Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element. - # Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере). - $tblNode = $node.SelectSingleNode("lf:Table", $ns) - if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode } - } - } - # DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть». - $di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di } - # title: "" — подавление авто-вывода: для типов, где компилятор вывел бы - # заголовок из имени, а в оригинале <Title> отсутствует. - if (-not $obj.Contains('title')) { - $autoTitle = $false - if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true } - elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) } - elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') } - if ($autoTitle) { $obj['title'] = '' } - } - Add-Layout $obj $node - Add-GenericScalars $obj $node - # extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) → - # строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink/events) → объект { text?, … }. - $etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns) - if ($etNode) { - $etTitle = $etNode.SelectSingleNode("lf:Title", $ns) - $textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null } - $etObj = [ordered]@{} - Add-Layout $etObj $etNode - Add-GenericScalars $etObj $etNode - Add-Appearance $etObj $etNode - if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true } - if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true } - if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true } - # События компаньона (напр. URLProcessing у hyperlink-подсказки) — переиспользуем механизм событий элемента - $etEv = Get-Events $etNode $name; if ($etEv) { $etObj['events'] = $etEv } - if ($etObj.Count -gt 0) { - if ($null -ne $textVal) { - if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } } - else { $etObj['text'] = $textVal } - # formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте - if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true } - } - $obj['extendedTooltip'] = $etObj - } elseif ($null -ne $textVal) { - $obj['extendedTooltip'] = $textVal - } - } - # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент) - $isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange') - $cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable - if ($null -ne $cb) { $obj['commandBar'] = $cb } - $cm = Decompile-CompanionPanel $node 'ContextMenu' - if ($null -ne $cm) { $obj['contextMenu'] = $cm } - return $obj -} - -# ───────────────────────────────────────────────────────────────────────────── -# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите. -# Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings. -function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') } -function PLD-Int { param($v) if ($null -eq $v) { return $null } return [int]$v } -# <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null. -function Get-PlannerValue { - param($node) - if (-not $node) { return $null } - if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null } - if ($node.InnerText) { return $node.InnerText } else { return $null } -} -function Build-PlannerFont { - param($node) - if (-not $node) { return $null } - $o = [ordered]@{} - foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { - $av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av } - } - if ($o.Count -eq 0) { return $null } - return $o -} -function Build-PlannerBorder { - param($node) - if (-not $node) { return $null } - $o = [ordered]@{} - $w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } - $st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText } - return $o -} -function Build-PlannerItem { - param($itn) - $o = [ordered]@{} - $valNode = $itn.SelectSingleNode("*[local-name()='value']") - if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText } - $o['text'] = (Get-Child $itn 'text') - $tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt } - $o['begin'] = (Get-Child $itn 'begin') - $o['end'] = (Get-Child $itn 'end') - $o['borderColor'] = (Get-Child $itn 'borderColor') - $o['backColor'] = (Get-Child $itn 'backColor') - $o['textColor'] = (Get-Child $itn 'textColor') - $fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } - $o['replacementDate'] = (Get-Child $itn 'replacementDate') - $o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted')) - $o['id'] = (Get-Child $itn 'id') - $o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted')) - $brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd } - $o['editMode'] = (Get-Child $itn 'editMode') - return $o -} -function Build-PlannerDimElement { - param($eln) - $o = [ordered]@{} - $v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } - $o['text'] = (Get-Child $eln 'text') - $o['borderColor'] = (Get-Child $eln 'borderColor') - $o['backColor'] = (Get-Child $eln 'backColor') - $o['textColor'] = (Get-Child $eln 'textColor') - $fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } - $subs = New-Object System.Collections.ArrayList - foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) } - if ($subs.Count -gt 0) { $o['elements'] = @($subs) } - $sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') } - $o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted')) - return $o -} -function Build-PlannerDimension { - param($dn) - $o = [ordered]@{} - $v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } - $o['text'] = (Get-Child $dn 'text') - $o['borderColor'] = (Get-Child $dn 'borderColor') - $o['backColor'] = (Get-Child $dn 'backColor') - $o['textColor'] = (Get-Child $dn 'textColor') - $fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } - $els = New-Object System.Collections.ArrayList - foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) } - if ($els.Count -gt 0) { $o['elements'] = @($els) } - $o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted')) - return $o -} -function Build-PlannerLevel { - param($lvn) - $o = [ordered]@{} - $o['measure'] = (Get-Child $lvn 'measure') - $o['interval'] = (PLD-Int (Get-Child $lvn 'interval')) - $o['show'] = (PLD-Bool (Get-Child $lvn 'show')) - $lineNode = $lvn.SelectSingleNode("*[local-name()='line']") - if ($lineNode) { - $ln = [ordered]@{} - $w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w } - $g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') } - $st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText } - $o['line'] = $ln - } - $o['scaleColor'] = (Get-Child $lvn 'scaleColor') - $o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule') - $fmtNode = $lvn.SelectSingleNode("*[local-name()='format']") - if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } } - $labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']") - if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } } - $o['backColor'] = (Get-Child $lvn 'backColor') - $o['textColor'] = (Get-Child $lvn 'textColor') - $o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels')) - return $o -} -function Build-PlannerTimeScale { - param($tsn) - $o = [ordered]@{} - $o['placement'] = (Get-Child $tsn 'placement') - $levels = New-Object System.Collections.ArrayList - foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) } - $o['levels'] = @($levels) - $o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent')) - $o['backColor'] = (Get-Child $tsn 'backColor') - $o['textColor'] = (Get-Child $tsn 'textColor') - $o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel')) - return $o -} -function Build-PlannerSettings { - param($setNode) - $pl = [ordered]@{} - $itemNodes = @($setNode.SelectNodes("*[local-name()='item']")) - if ($itemNodes.Count -gt 0) { - $items = New-Object System.Collections.ArrayList - foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) } - $pl['items'] = @($items) - } - $dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']")) - if ($dimNodes.Count -gt 0) { - $dims = New-Object System.Collections.ArrayList - foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) } - $pl['dimensions'] = @($dims) - } - $pl['borderColor'] = (Get-Child $setNode 'borderColor') - $pl['backColor'] = (Get-Child $setNode 'backColor') - $pl['textColor'] = (Get-Child $setNode 'textColor') - $pl['lineColor'] = (Get-Child $setNode 'lineColor') - $fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt } - $pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod') - $pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod') - $pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale')) - $pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders')) - $pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders')) - $wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']") - if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } } - $pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit') - $pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition')) - $pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent')) - $pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent')) - $tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']") - if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) } - $perNode = $setNode.SelectSingleNode("*[local-name()='period']") - if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } } - $pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate')) - $pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation') - $pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient') - $pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth')) - $pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight')) - $pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth')) - $pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight')) - $pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader') - $pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader') - $brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd } - $pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType') - return $pl -} - -# ───────────────────────────────────────────────────────────────────────────── -# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок: -# рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии) -# детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML), -# серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип). -$CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } -$CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 } -$CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } -function Conv-ChartScalar { - param([string]$v) - if ($v -eq 'true') { return $true } - if ($v -eq 'false') { return $false } - return $v -} -function Build-ChartNode { - param($n, [string]$name) - # ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени) - if ($CHART_ML_FIELDS.Contains($name)) { - $ml = Get-LangText $n - if ($null -eq $ml) { return '' } else { return $ml } - } - $kids = @($n.SelectNodes("*")) - if ($kids.Count -eq 0) { - # лист: attrs-only (шрифт/gaugeQualityBands) или текст - $attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' }) - if ($attrs.Count -gt 0) { - $o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o - } - return (Conv-ChartScalar $n.InnerText) - } - # line/border: дочерний v8ui:style (+ width[/gap]) - $styleChild = $n.SelectSingleNode("*[local-name()='style']") - if ($styleChild) { - $o = [ordered]@{} - $w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } - $g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') } - $o['style'] = $styleChild.InnerText - return $o - } - # вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени - $o = [ordered]@{} - foreach ($c in $kids) { - $ln = $c.LocalName - $val = Build-ChartNode $c $ln - if ($CHART_SERIES_FIELDS.Contains($ln)) { - if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList } - [void]$o[$ln].Add($val) - } elseif ($o.Contains($ln)) { - if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp } - [void]$o[$ln].Add($val) - } else { - $o[$ln] = $val - } - } - # нормализуем ArrayList → @() для сериализации - foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } } - return $o -} -function Build-ChartSettings { - param($setNode) - return (Build-ChartNode $setNode '') -} - -# --- 5. Form-level assembly --- -$dsl = [ordered]@{} - -$titleNode = $root.SelectSingleNode("lf:Title", $ns) -if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } } - -# properties (прямые скаляры под <Form>, PascalCase → camelCase) -$KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance','ShowCloseButton','HorizontalAlign','ChildrenAlign','ShowTitle','ConversationsRepresentation','CollapseItemsByImportanceVariant','GroupList','ChildItemsWidth','VerticalAlign','HorizontalSpacing') -$props = [ordered]@{} -foreach ($pn in $KNOWN_FORM_PROPS) { - $v = Get-Child $root $pn - if ($null -ne $v) { - $camel = $pn.Substring(0,1).ToLower() + $pn.Substring(1) - if ($v -eq 'true') { $props[$camel] = $true } - elseif ($v -eq 'false') { $props[$camel] = $false } - elseif ($v -match '^\d+$') { $props[$camel] = [int]$v } - else { $props[$camel] = $v } - } -} -# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим: -# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title); -# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить). -if ($dsl.Contains('title')) { - if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' } - elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') } -} -if ($props.Count -gt 0) { $dsl['properties'] = $props } - -# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок -$mdcb = $root.SelectSingleNode("lf:MobileDeviceCommandBarContent", $ns) -if ($mdcb) { - $names = New-Object System.Collections.ArrayList - foreach ($it in @($mdcb.SelectNodes("xr:Item", $ns))) { - $v = $it.SelectSingleNode("xr:Value", $ns); if ($v) { [void]$names.Add($v.InnerText) } - } - if ($names.Count -gt 0) { $dsl['mobileCommandBarContent'] = @($names) } -} - -# excludedCommands (form-level <CommandSet>) -$csForm = $root.SelectSingleNode("lf:CommandSet", $ns) -if ($csForm) { - $excForm = New-Object System.Collections.ArrayList - foreach ($ec in @($csForm.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$excForm.Add($ec.InnerText) } - if ($excForm.Count -gt 0) { $dsl['excludedCommands'] = @($excForm) } -} - -# events (form-level) -$evForm = Get-Events $root $null -if ($evForm) { - # form-level: компилятор хранит как {Event: handler} напрямую - $evMap = [ordered]@{} - $evNode = $root.SelectSingleNode("lf:Events", $ns) - foreach ($e in @($evNode.SelectNodes("lf:Event", $ns))) { $evMap[$e.GetAttribute("name")] = $e.InnerText } - if ($evMap.Count -gt 0) { $dsl['events'] = $evMap } -} - -# Зеркало компилятор-эвристики B3 (Compute-MainAcbAutofill): наличие cmdBar-элемента где-либо -# в дереве → форменный AutoCommandBar autofill=false. Нужно, чтобы предсказать вывод и выразить -# отклонение (голый корень при наличии cmdBar). -function Test-AnyCmdBar { - param($list) - if (-not $list) { return $false } - foreach ($e in $list) { - if ($e -is [System.Collections.IDictionary] -and $e.Contains('cmdBar')) { return $true } - if ($e -is [System.Collections.IDictionary]) { - if ($e.Contains('children') -and (Test-AnyCmdBar $e['children'])) { return $true } - if ($e.Contains('columns') -and (Test-AnyCmdBar $e['columns'])) { return $true } - } - } - return $false -} - -# elements (+ форменный AutoCommandBar как autoCmdBar-элемент, если у него есть содержимое/отклонение) -$elemList = New-Object System.Collections.ArrayList -$elements = Decompile-Children $root -$formHasCmdBar = Test-AnyCmdBar $elements -$acb = $root.SelectSingleNode("lf:AutoCommandBar", $ns) -if ($acb) { - $haln = Get-Child $acb 'HorizontalAlign' - $acbAutofill = Get-Child $acb 'Autofill' - $acbKids = Decompile-Children $acb - $acbObj = $null - if ($haln -or ($acbAutofill -eq 'false') -or $acbKids) { - $acbObj = [ordered]@{} - $acbObj['autoCmdBar'] = $acb.GetAttribute("name") - if ($haln) { $acbObj['horizontalAlign'] = $haln } - if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false } - if ($acbKids) { $acbObj['children'] = $acbKids } - } elseif ($formHasCmdBar -and $null -eq $acbAutofill) { - # Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер - $acbObj = [ordered]@{} - $acbObj['autoCmdBar'] = $acb.GetAttribute("name") - $acbObj['autofill'] = $true - } - if ($acbObj) { [void]$elemList.Add($acbObj) } -} -if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } } -if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) } - -# attributes -# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3. -function Test-IsObjectLikeTypeDec([string]$type) { - if ([string]::IsNullOrEmpty($type)) { return $false } - if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true } - return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.') -} -$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns) -if ($attrsNode) { - $attrs = New-Object System.Collections.ArrayList - # Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно - # один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет) - # → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно. - $allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns)) - $anyMainAttr = $false; $objLikeNodes = @() - foreach ($an in $allAttrNodes) { - if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true } - $atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns)) - if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an } - } - $suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null } - foreach ($a in $allAttrNodes) { - $ao = [ordered]@{} - $ao['name'] = $a.GetAttribute("name") - $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } - # valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList - # (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно. - $setNode = $a.SelectSingleNode("lf:Settings", $ns) - if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') { - $vt = Decompile-Type $setNode - $ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер "" - } - # Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват). - elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') { - $ao['planner'] = Build-PlannerSettings $setNode - } - # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart"> → chart (генерик). - elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:(Gantt)?Chart$') { - $ao['chart'] = Build-ChartSettings $setNode - } - if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } - elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false } - $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } - $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } - # Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок - # из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем - # ключ (компилятор воспроизведёт); иначе → явный заголовок. - $isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title - $tNode = $a.SelectSingleNode("lf:Title", $ns) - if ($tNode) { - $t = Get-LangText $tNode - if ($null -ne $t) { - if ($isMain -or -not ($t -is [string]) -or $t -ne (Title-FromName $ao['name'])) { $ao['title'] = $t } - } - } elseif (-not $isMain) { - $ao['title'] = '' - } - # SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved: - # Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал - # тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute). - if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } - elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false } - # Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true; - # иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив. - $saveNode = $a.SelectSingleNode("lf:Save", $ns) - if ($saveNode) { - $nm = "$($ao['name'])" - $flds = @($saveNode.SelectNodes("lf:Field", $ns) | ForEach-Object { $_.InnerText }) - if ($flds.Count -eq 1 -and $flds[0] -eq $nm) { - $ao['save'] = $true - } elseif ($flds.Count -gt 0) { - $stripped = @($flds | ForEach-Object { - if ($_ -match "^$([regex]::Escape($nm))\.(.+)$") { $matches[1] } else { $_ } - }) - if ($stripped.Count -eq 1) { $ao['save'] = $stripped[0] } else { $ao['save'] = $stripped } - } - } - $fc = Get-Child $a 'FillCheck'; if ($fc) { $ao['fillCheck'] = $fc } - $afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo } - $colsNode = $a.SelectSingleNode("lf:Columns", $ns) - if ($colsNode) { - $cols = New-Object System.Collections.ArrayList - foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) { [void]$cols.Add((Decompile-AttrColumn $c)) } - if ($cols.Count -gt 0) { $ao['columns'] = @($cols) } - # AdditionalColumns: доп. колонки табличных частей объекта (группа на табличную часть) - $addNodes = @($colsNode.SelectNodes("lf:AdditionalColumns", $ns)) - if ($addNodes.Count -gt 0) { - $addList = New-Object System.Collections.ArrayList - foreach ($an in $addNodes) { - $acObj = [ordered]@{}; $acObj['table'] = $an.GetAttribute("table") - $acCols = New-Object System.Collections.ArrayList - foreach ($c in @($an.SelectNodes("lf:Column", $ns))) { [void]$acCols.Add((Decompile-AttrColumn $c)) } - $acObj['columns'] = @($acCols) - [void]$addList.Add($acObj) - } - $ao['additionalColumns'] = @($addList) - } - } - # UseAlways: поля, всегда читаемые. Префикс "ИмяРеквизита." снимаем. - # ValueTable (есть columns): useAlways:true на совпавшей колонке; остальные → массив атрибута. - # Дин-список/прочие (нет columns): массив useAlways на атрибуте. - $uaNode = $a.SelectSingleNode("lf:UseAlways", $ns) - if ($uaNode) { - $prefix = "$($ao['name'])." - $shorts = New-Object System.Collections.ArrayList - foreach ($fn in @($uaNode.SelectNodes("lf:Field", $ns))) { - $t = $fn.InnerText.Trim() - # Снимаем префикс "ИмяРеквизита.". Маркер "~" (query-поле дин-списка) сохраняем, - # префикс снимаем ПОСЛЕ него: ~Список.Остановлен → ~Остановлен (компилятор развернёт обратно). - if ($t.StartsWith('~')) { - $rest = $t.Substring(1) - if ($rest.StartsWith($prefix)) { $rest = $rest.Substring($prefix.Length) } - $t = "~$rest" - } elseif ($t.StartsWith($prefix)) { - $t = $t.Substring($prefix.Length) - } - [void]$shorts.Add($t) - } - if ($ao.Contains('columns')) { - $rest = New-Object System.Collections.ArrayList - foreach ($s in $shorts) { - $col = $ao['columns'] | Where-Object { $_['name'] -eq $s } | Select-Object -First 1 - if ($col) { $col['useAlways'] = $true } else { [void]$rest.Add($s) } - } - if ($rest.Count -gt 0) { $ao['useAlways'] = @($rest) } - } elseif ($shorts.Count -gt 0) { - $ao['useAlways'] = @($shorts) - } - } - # Settings динамического списка (только xsi:type=DynamicList; Planner/TypeDescription — выше) - $setNode = $a.SelectSingleNode("lf:Settings", $ns) - if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'DynamicList$') { - $so = [ordered]@{} - # AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть». - $afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') } - $mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt } - # AutoSaveUserSettings — авто-сохранение польз. настроек дин-списка (в корпусе только false; - # дефолт true → платформа эмитит отклонение). Захват факт. значения. - $asus = Get-Child $setNode 'AutoSaveUserSettings'; if ($null -ne $asus) { $so['autoSaveUserSettings'] = ($asus -eq 'true') } - $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns) - if ($qtNode -and $qtNode.InnerText) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" } - # DynamicDataRead: дефолт true → эмитим только false - if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false } - # Явные поля набора (редко, ~4.5%) — захват только при наличии Field - $fieldNodes = @($setNode.SelectNodes("lf:Field", $ns)) - if ($fieldNodes.Count -gt 0) { - $fields = New-Object System.Collections.ArrayList - foreach ($fn in $fieldNodes) { - $fo = [ordered]@{} - $fld = Get-Child $fn 'field' - $dp = Get-Child $fn 'dataPath' - if ($fld) { $fo['field'] = $fld } - if ($dp -and $dp -ne $fld) { $fo['dataPath'] = $dp } - $ftn = $fn.SelectSingleNode("dcssch:title", $ns) - if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } } - [void]$fields.Add($fo) - } - $so['fields'] = @($fields) - } - # Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings) - $paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns)) - if ($paramNodes.Count -gt 0) { - $dlPars = New-Object System.Collections.ArrayList - foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) } - $so['parameters'] = @($dlPars) - } - # ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор - # регенерит каноничный скелет. Захватываем только контейнеры с реальными - # dcsset:item (filter/order/conditionalAppearance) в формат компилятора. - $lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns) - if ($lsNode) { - $fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns) - if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) { - $flt = @() - foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { - $bi = (Build-FilterItem -itemNode $fc -loc "settings/filter") - if ($null -ne $bi) { $flt += $bi } - } - if ($flt.Count -gt 0) { $so['filter'] = @($flt) } - } - $oNode = $lsNode.SelectSingleNode("dcsset:order", $ns) - if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) { - $ord = Build-Order -ordNode $oNode -loc "settings/order" - if (@($ord).Count -gt 0) { $so['order'] = @($ord) } - } - $caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns) - if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) { - $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance" - if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) } - } - # Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в - # настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters. - $dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns) - if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) { - $dp = Build-FormDataParameters $dpNode - if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) } - } - # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). - # Канон → $null (компилятор регенерит полный скелет, как раньше). - $lsShape = Get-ListSettingsShape $lsNode - if ($null -ne $lsShape) { $so['listSettings'] = $lsShape } - } - if ($so.Count -gt 0) { $ao['settings'] = $so } - } - [void]$attrs.Add($ao) - } - if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) } -} - -# conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>; -# та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance) -if ($attrsNode) { - $caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns) - if ($caNode) { - $ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance" - if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) } - } -} - -# parameters -$parsNode = $root.SelectSingleNode("lf:Parameters", $ns) -if ($parsNode) { - $pars = New-Object System.Collections.ArrayList - foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) { - $po = [ordered]@{}; $po['name'] = $p.GetAttribute("name") - $ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty } - if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true } - [void]$pars.Add($po) - } - if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) } -} - -# commands -$cmdsNode = $root.SelectSingleNode("lf:Commands", $ns) -if ($cmdsNode) { - $cmds = New-Object System.Collections.ArrayList - foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) { - $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") - $act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act } - if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true } - # Заголовок команды: есть <Title> → захват; нет → суппресс-маркер "" (иначе компилятор - # додумает из имени — авто-вывод неверен для ~0.13% команд без заголовка в оригинале). - $tNode = $c.SelectSingleNode("lf:Title", $ns) - if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } - else { $co['title'] = '' } - $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } - $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } - $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } - $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } - # Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>) - $ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate } - $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } - Set-CommandPicture $co $c - $rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep } - [void]$cmds.Add($co) - } - if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) } -} - -# commandInterface (форменный <CommandInterface> — последний дочерний Form) -$ci = Decompile-CommandInterface -if ($null -ne $ci) { $dsl['commandInterface'] = $ci } - -# --- 6. Output --- -$json = ConvertTo-CompactJson -obj $dsl -if ($OutputPath) { - [System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false))) - Save-QueryFiles - Write-Host "form-decompile: $OutputPath" -} else { - Write-Output $json -} +# form-decompile v0.111 — Decompile 1C managed Form.xml to JSON DSL (draft) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$FormPath, + + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 0. Resolve and validate input --- +if (-not (Test-Path $FormPath)) { + Write-Error "Form not found: $FormPath" + exit 1 +} +$FormPath = (Resolve-Path $FormPath).Path + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load($FormPath) +$root = $xmlDoc.DocumentElement + +# Ring 2: not a managed Form +if ($root.LocalName -ne 'Form') { + [Console]::Error.WriteLine("form-decompile: корневой элемент <$($root.LocalName)> не <Form> — это не управляемая форма.") + exit 2 +} + +# --- 1. Namespaces --- +$NS_LF = "http://v8.1c.ru/8.3/xcf/logform" +$NS_V8 = "http://v8.1c.ru/8.1/data/core" +$NS_XR = "http://v8.1c.ru/8.3/xcf/readable" +$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" + +$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings" +$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema" +$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core" +$NS_V8UI = "http://v8.1c.ru/8.1/data/ui" +$NS_APP = "http://v8.1c.ru/8.2/managed-application/core" + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("lf", $NS_LF) +$ns.AddNamespace("v8", $NS_V8) +$ns.AddNamespace("xr", $NS_XR) +$ns.AddNamespace("xsi", $NS_XSI) +$ns.AddNamespace("dcsset", $NS_DCSSET) +$ns.AddNamespace("dcssch", $NS_DCSSCH) +$ns.AddNamespace("dcscor", $NS_DCSCOR) +$ns.AddNamespace("v8ui", $NS_V8UI) +$ns.AddNamespace("app", $NS_APP) + +# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). +# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе, +# компилятор регенерит тот же скелет → чистый раундтрип. +$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb' +$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4' +$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3' +$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec' + +# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) --- +$script:outputDir = $null +$script:outputBasename = $null +if ($OutputPath) { + $od = Split-Path -Parent $OutputPath + if (-not $od) { $od = (Get-Location).Path } + $script:outputDir = $od + $script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath) +} +$script:queryFilesAccumulator = @() +$script:queryFileNamesUsed = @{} + +# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file". +function Maybe-ExternalizeQuery { + param([string]$queryText, [string]$listName) + if (-not $queryText) { return $queryText } + if (-not $script:outputDir) { return $queryText } + $lineCount = ([regex]::Matches($queryText, "`n")).Count + 1 + if ($lineCount -lt 3) { return $queryText } + $safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' } + $prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' } + $fileName = "$prefix$safe.sql" + $suffix = 1 + while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" } + $script:queryFileNamesUsed[$fileName] = $true + $script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText } + return "@$fileName" +} +function Save-QueryFiles { + if ($script:queryFilesAccumulator.Count -eq 0) { return } + if (-not $script:outputDir) { return } + $enc = New-Object System.Text.UTF8Encoding($false) + foreach ($qf in $script:queryFilesAccumulator) { + [System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc) + } + [Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)") +} + +# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/ +# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false: +# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness +# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже). +function Test-ListSettingsHasContent { + param($lsNode) + if (-not $lsNode) { return $false } + foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) { + $cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns) + if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true } + } + return $false +} + +# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance → +# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма == +# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы +# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора. +function Get-ListSettingsShape { + param($lsNode) + if (-not $lsNode) { return $null } + $shape = [ordered]@{} + foreach ($child in $lsNode.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + $tag = $child.LocalName + if ($tag -in @('filter','order','conditionalAppearance')) { + $hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns) + $hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns) + $shape[$tag] = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" + } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } + elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } + else { return $null } # item/dataParameters/itemsUserSettingPresentation/… → канон-fallback + } + # Полный каноничный скелет → опускаем (компилятор регенерит) + if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and ` + $shape['conditionalAppearance'] -eq 'vu' -and $shape['itemsViewMode'] -eq $true -and $shape['itemsUserSettingID'] -eq $true) { return $null } + return $shape +} + +# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) --- +function Fail-Ring3 { + param([string]$kind, [string]$loc) + [Console]::Error.WriteLine("form-decompile: декомпиляция пока не поддерживает $kind (path: $loc)") + [Console]::Error.WriteLine("Для точечной работы с этой формой используй /form-edit.") + exit 3 +} +# ConditionalAppearance со scope (привязка к области) пока не воспроизводим — fail-ring3 только в этом случае. +foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*[local-name()='item']/*[local-name()='scope'][node()]")) { Fail-Ring3 -kind "ConditionalAppearance со scope" -loc "form/ConditionalAppearance/item/scope" } +# Реквизит с design-time конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… — +# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие +# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча. +foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) { + $st = $s.GetAttribute("type", $NS_XSI) + if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') { + Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings" + } + # Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с + # префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые + # дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны. + elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) { + Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings" + } +} + +# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) --- +function Convert-StringToJsonLiteral { + param([string]$s) + if ($null -eq $s) { return 'null' } + $sb = New-Object System.Text.StringBuilder + [void]$sb.Append('"') + foreach ($ch in $s.ToCharArray()) { + $code = [int]$ch + if ($code -eq 0x22) { [void]$sb.Append('\"') } + elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } + elseif ($code -eq 0x08) { [void]$sb.Append('\b') } + elseif ($code -eq 0x09) { [void]$sb.Append('\t') } + elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } + elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } + elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } + elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } + else { [void]$sb.Append($ch) } + } + [void]$sb.Append('"') + return $sb.ToString() +} +function Try-InlineJson { + param($obj) + if ($null -eq $obj) { return 'null' } + if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } + if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } + if ($obj -is [int] -or $obj -is [long]) { return "$obj" } + if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { + return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) + } + if ($obj -is [System.Collections.IDictionary]) { + if ($obj.Count -eq 0) { return '{}' } + $parts = @() + foreach ($k in $obj.Keys) { + $v = Try-InlineJson $obj[$k] + if ($null -eq $v) { return $null } + $parts += "$(Convert-StringToJsonLiteral "$k"): $v" + } + return '{ ' + ($parts -join ', ') + ' }' + } + if ($obj -is [array] -or $obj -is [System.Collections.IList]) { + $items = @($obj) + if ($items.Count -eq 0) { return '[]' } + $parts = @() + foreach ($it in $items) { + $v = Try-InlineJson $it + if ($null -eq $v) { return $null } + $parts += $v + } + return '[' + ($parts -join ', ') + ']' + } + return $null +} +function ConvertTo-CompactJson { + param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120) + $indent = $indentUnit * $depth + $childIndent = $indentUnit * ($depth + 1) + if ($null -eq $obj) { return 'null' } + if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } + if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } + if ($obj -is [int] -or $obj -is [long]) { return "$obj" } + if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { + return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) + } + $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) + if ($isContainer) { + $inlineAttempt = Try-InlineJson $obj + if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt } + } + if ($obj -is [System.Collections.IDictionary]) { + $keys = @($obj.Keys) + if ($keys.Count -eq 0) { return '{}' } + $parts = @() + foreach ($k in $keys) { + $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit + $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" + } + return "{`n" + ($parts -join ",`n") + "`n$indent}" + } + if ($obj -is [array] -or $obj -is [System.Collections.IList]) { + $items = @($obj) + if ($items.Count -eq 0) { return '[]' } + $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) + return "[`n" + ($parts -join ",`n") + "`n$indent]" + } + return (Convert-StringToJsonLiteral "$obj") +} + +# --- 2. Helpers --- + +# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей. +# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы +# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions). +$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar') + +# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...} +function Get-LangText { + param($node) + if ($null -eq $node) { return $null } + $items = @($node.SelectNodes("v8:item", $ns)) + if ($items.Count -eq 0) { return $null } + $map = [ordered]@{} + foreach ($it in $items) { + $lang = $it.SelectSingleNode("v8:lang", $ns) + $content = $it.SelectSingleNode("v8:content", $ns) + if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } + } + if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] } + return $map +} + +# Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает +# <v8:content> </v8:content> → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой +# Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue). +# Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель +# «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст. +function Get-LangTextWS { + param($node) + $t = Get-LangText $node + if ($null -eq $t) { return $null } + if ($t -is [string]) { + if ($t -eq '' -and $node.SelectSingleNode("v8:item/v8:content", $ns)) { return ' ' } + return $t + } + foreach ($it in @($node.SelectNodes("v8:item", $ns))) { + $lang = $it.SelectSingleNode("v8:lang", $ns) + $content = $it.SelectSingleNode("v8:content", $ns) + if ($lang -and $content -and $t.Contains($lang.InnerText) -and $t[$lang.InnerText] -eq '') { + $t[$lang.InnerText] = ' ' + } + } + return $t +} + +# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile. +# Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам. +function Title-FromName { + param([string]$name) + if (-not $name) { return '' } + $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') + $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') + $parts = $s -split ' ' + if ($parts.Count -eq 0) { return $s } + $out = New-Object System.Collections.ArrayList + [void]$out.Add($parts[0]) + for ($i = 1; $i -lt $parts.Count; $i++) { + $p = $parts[$i] + if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) } + else { [void]$out.Add($p.ToLower()) } + } + return ($out -join ' ') +} + +# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!). +$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' +function Test-HasRealMarkup { + param($text) + if ($null -eq $text) { return $false } + $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") } + foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } + return $false +} +# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted +# совпал с атрибутом; иначе явный {text, formatted}. +function Get-MLFormattedValue { + param($titleNode) + if (-not $titleNode) { return $null } + # Get-LangTextWS восстанавливает значимый пробел в <v8:content> </v8:content> (одиночный и + # мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "". + $text = Get-LangTextWS $titleNode + if ($null -eq $text) { return $null } + $fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true') + if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text } + $o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o +} + +# Прочитать дочерний скаляр (по local-name, без namespace) +function Get-Child { + param($node, [string]$name) + $c = $node.SelectSingleNode("*[local-name()='$name']") + if ($c) { return $c.InnerText } else { return $null } +} +function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") } +function To-Bool { param([string]$v) return ($v -eq 'true') } + +# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка). +# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type. +function Convert-TypedValue { + param([string]$raw, [string]$xsiType) + switch -regex ($xsiType) { + 'decimal$' { + if ($raw -match '^-?\d+$') { return [int]$raw } + return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture) + } + 'boolean$' { return ($raw -eq 'true') } + default { return $raw } + } +} + +# ===================================================================== +# Захват настроек компоновщика динамического списка (ListSettings): +# filter / order / conditionalAppearance. Логика портирована из навыка +# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance +# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd +# заменён на запись в stderr + пропуск элемента (form-decompile — draft, +# скрипт не падает на непокрытых конструкциях). +# ===================================================================== + +# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text. +function Get-Text { + param($node, [string]$xpath) + if (-not $node) { return $null } + if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText } + $n = $node.SelectSingleNode($xpath, $ns) + if ($n) { return $n.InnerText } else { return $null } +} + +# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash. +# Алиас на уже существующий Get-LangText (тот же контракт). +function Get-MLText { param($node) return (Get-LangText $node) } + +# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string. +# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText. +function Get-PresText { + param($node) + if (-not $node) { return $null } + $ml = Get-MLText $node + if ($null -ne $ml) { return $ml } + if ($node.InnerText) { return $node.InnerText } + return $null +} + +# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo") +function Get-LocalXsiType { + param($node) + if (-not $node) { return $null } + $t = $node.GetAttribute("type", $NS_XSI) + if ($t -match ':(.+)$') { return $matches[1] } + return $t +} + +# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile). +function Get-FontValue { + param($valNode) + $f = [ordered]@{ '@type' = 'Font' } + foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $a = $valNode.Attributes[$attrName] + if ($null -ne $a) { $f[$attrName] = $a.Value } + } + return $f +} + +# Линия (граница) оформления → объект {@type:Line, width, gap, style}. +function Get-LineValue { + param($valNode) + $obj = [ordered]@{ '@type' = 'Line' } + $w = $valNode.GetAttribute("width") + $g = $valNode.GetAttribute("gap") + if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } + if ($g -ne '') { $obj['gap'] = ($g -eq 'true') } + $styleNode = $valNode.SelectSingleNode("v8ui:style", $ns) + if ($styleNode) { $obj['style'] = $styleNode.InnerText } + return $obj +} + +# Прочитать <dcscor:value> в JSON-значение: Font/Line/Field/multilang/raw text. +function Read-AppearanceValueNode { + param($valNode) + if (-not $valNode) { return $null } + $vt = Get-LocalXsiType $valNode + if ($vt -eq 'LocalStringType') { + # НЕ схлопываем одноязычный в строку: значение параметра оформления различает + # xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы + # одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType. + $map = [ordered]@{} + foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) { + $lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns) + if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } + } + return $map + } + if ($vt -eq 'Font') { return (Get-FontValue $valNode) } + if ($vt -eq 'Line') { return (Get-LineValue $valNode) } + # dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь} + if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } } + return $valNode.InnerText +} + +# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd). +$script:filterOpMap = @{ + 'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>='; + 'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn'; + 'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy'; + 'Contains'='contains'; 'NotContains'='notContains'; + 'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith'; + 'Like'='like'; 'NotLike'='notLike'; + 'Filled'='filled'; 'NotFilled'='notFilled' +} + +# Render filter value node → shorthand-acceptable scalar string +function Get-FilterValue { + param($valNode) + if (-not $valNode) { return '_' } + $nil = $valNode.GetAttribute("nil", $NS_XSI) + if ($nil -eq 'true') { return '_' } + $vType = Get-LocalXsiType $valNode + if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText } + if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) } + $txt = $valNode.InnerText + if (-not $txt) { return '_' } + return $txt +} + +# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field). +function Get-FilterValueWithType { + param($valNode) + if (-not $valNode) { return @{ value = '_'; type = $null } } + $rawType = $valNode.GetAttribute("type", $NS_XSI) + $nil = $valNode.GetAttribute("nil", $NS_XSI) + if ($nil -eq 'true') { return @{ value = '_'; type = $null } } + $vType = Get-LocalXsiType $valNode + if ($vType -eq 'LocalStringType') { + return @{ value = (Get-MLText $valNode); type = $rawType } + } + # Стандартная дата начала/окончания. Формы (от самой компактной): + # SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт + # даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType; + # SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал. + if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') { + $variantN = $valNode.SelectSingleNode("v8:variant", $ns) + $dateN = $valNode.SelectSingleNode("v8:date", $ns) + $variantStr = if ($variantN) { $variantN.InnerText } else { '' } + if ($dateN) { + if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') { + return @{ value = $dateN.InnerText; type = $null } # голая дата = SBD Custom шорткат + } + return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType } + } + return @{ value = $variantStr; type = $rawType } # именованный вариант + } + $txt = $valNode.InnerText + if (-not $txt) { return @{ value = '_'; type = $rawType } } + if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } } + if ($vType -eq 'decimal') { + if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } } + return @{ value = [double]$txt; type = $rawType } + } + return @{ value = $txt; type = $rawType } +} + +# Convert filter item node → shorthand string или object form (рекурсивно для групп). +function Build-FilterItem { + param($itemNode, [string]$loc) + $xtype = Get-LocalXsiType $itemNode + if ($xtype -eq 'FilterItemGroup') { + $gt = Get-Text $itemNode "dcsset:groupType" + $groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } } + $items = @() + foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-FilterItem -itemNode $c -loc "$loc/item") + if ($null -ne $bi) { $items += $bi } + } + $gObj = [ordered]@{ group = $groupName; items = $items } + $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) + if ($gPresNode) { + $gPres = Get-MLText $gPresNode + if (-not $gPres) { $gPres = $gPresNode.InnerText } + if ($gPres) { $gObj['presentation'] = $gPres } + } + $gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) + if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText } + $gUSID = Get-Text $itemNode "dcsset:userSettingID" + if ($gUSID) { $gObj['userSettingID'] = 'auto' } + $gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($gUSPN) { + $gUSP = Get-PresText $gUSPN + if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP } + } + return $gObj + } + if ($xtype -ne 'FilterItemComparison') { + [Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)") + return $null + } + $leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns) + $field = if ($leftNode) { $leftNode.InnerText } else { $null } + $ct = Get-Text $itemNode "dcsset:comparisonType" + $op = $script:filterOpMap[$ct] + if (-not $op) { $op = $ct } + + $rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns)) + $value = $null + $valueIsArrayFlag = $false + $valueTypeAttr = $null + if ($rightNodes.Count -eq 1) { + $rn = $rightNodes[0] + if ((Get-LocalXsiType $rn) -eq 'ValueListType') { + $value = @() + $valueIsArrayFlag = $true + } else { + $vt = Get-FilterValueWithType $rn + $value = $vt.value + $autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and ` + ("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') + if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) { + $valueTypeAttr = $vt.type + } + } + } elseif ($rightNodes.Count -gt 1) { + $arr = @() + $rawTypes = @() + foreach ($rn in $rightNodes) { + $arr += (Get-FilterValue $rn) + $rawTypes += $rn.GetAttribute("type", $NS_XSI) + } + $value = $arr + $valueIsArrayFlag = $true + $uniqTypes = @($rawTypes | Sort-Object -Unique) + if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) { + $autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and ` + ($arr.Count -gt 0) -and ` + (@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0) + if (-not $autoDetectsDTV) { + $valueTypeAttr = $uniqTypes[0] + } + } + } + + $use = Get-Text $itemNode "dcsset:use" + $userId = Get-Text $itemNode "dcsset:userSettingID" + $vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns) + $viewMode = if ($vmNode) { $vmNode.InnerText } else { $null } + $userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns) + $fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) + $fiPres = $null + if ($fiPresNode) { + $fiPres = Get-MLText $fiPresNode + if (-not $fiPres) { $fiPres = $fiPresNode.InnerText } + } + + $flags = @() + if ($use -eq 'false') { $flags += '@off' } + if ($userId) { $flags += '@user' } + if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' } + elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' } + elseif ($viewMode -eq 'Normal') { $flags += '@normal' } + + $noValueOps = @('filled','notFilled') + + if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres) { + $obj = [ordered]@{ field = $field; op = $op } + if ($op -notin $noValueOps -and $null -ne $value) { + if ($valueIsArrayFlag) { + $arrAsList = New-Object System.Collections.ArrayList + foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) } + $obj['value'] = $arrAsList + } else { + $obj['value'] = $value + } + } + if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr } + if ($use -eq 'false') { $obj['use'] = $false } + if ($userId) { $obj['userSettingID'] = 'auto' } + if ($fiPres) { $obj['presentation'] = $fiPres } + if ($viewMode) { $obj['viewMode'] = $viewMode } + if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode } + return $obj + } + + $s = $field + if ($op -in $noValueOps) { + $s += " $op" + } else { + $vDisplay = '_' + if ($null -ne $value) { + if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } } + elseif ("$value" -ne '') { $vDisplay = "$value" } + } + $s += " $op $vDisplay" + } + if ($flags) { $s += ' ' + ($flags -join ' ') } + return $s +} + +# Рекурсивный хелпер одного элемента selection (для conditionalAppearance). +function Build-SelectionItem { + param($item, [string]$loc) + $xt = Get-LocalXsiType $item + if (-not $xt) { + $fName = Get-Text $item "dcsset:field" + if ($fName) { return $fName } + $fieldEl = $item.SelectSingleNode("dcsset:field", $ns) + if ($fieldEl) { return 'Auto' } + } + switch ($xt) { + 'SelectedItemAuto' { + $useV = Get-Text $item "dcsset:use" + if ($useV -eq 'false') { + return [ordered]@{ auto = $true; use = $false } + } + return 'Auto' + } + 'SelectedItemField' { + $fName = Get-Text $item "dcsset:field" + $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) + $title = Get-MLText $titleNode + $vmN = $item.SelectSingleNode("dcsset:viewMode", $ns) + $useV = Get-Text $item "dcsset:use" + $useFalse = ($useV -eq 'false') + if ($title -or $vmN -or $useFalse) { + $obj = [ordered]@{ field = $fName } + if ($useFalse) { $obj['use'] = $false } + if ($title) { $obj['title'] = $title } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + return $obj + } + return $fName + } + 'SelectedItemFolder' { + $titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns) + $folderTitle = Get-MLText $titleNode + $inner = @() + foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-SelectionItem -item $sub -loc "$loc/folder") + if ($null -ne $bi) { $inner += $bi } + } + $entry = [ordered]@{ folder = $folderTitle; items = $inner } + $folderField = Get-Text $item "dcsset:field" + if ($folderField) { $entry['field'] = $folderField } + $plN = $item.SelectSingleNode("dcsset:placement", $ns) + if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') { + $entry['placement'] = $plN.InnerText + } + return $entry + } + default { + [Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)") + return $null + } + } +} + +# Build selection items array (для conditionalAppearance). +function Build-Selection { + param($selNode, [string]$loc) + if (-not $selNode) { return @() } + $out = @() + foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-SelectionItem -item $it -loc $loc) + if ($null -ne $bi) { $out += $bi } + } + return ,$out +} + +# Build order items array. +function Build-Order { + param($ordNode, [string]$loc) + if (-not $ordNode) { return @() } + $out = @() + foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) { + $xt = Get-LocalXsiType $it + switch ($xt) { + 'OrderItemAuto' { $out += 'Auto' } + 'OrderItemField' { + $fn = Get-Text $it "dcsset:field" + $ot = Get-Text $it "dcsset:orderType" + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + $useV = Get-Text $it "dcsset:use" + $useFalse = ($useV -eq 'false') + if ($vmN -or $useFalse) { + $obj = [ordered]@{ field = $fn } + if ($useFalse) { $obj['use'] = $false } + if ($ot -eq 'Desc') { $obj['direction'] = 'desc' } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + $out += $obj + } else { + if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn } + } + } + default { + [Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)") + } + } + } + return ,$out +} + +# Build appearance dict из <dcsset:appearance> (Line/Font/multilang/nested items). +function Get-SettingsAppearance { + param($appNode) + if (-not $appNode) { return $null } + $dict = [ordered]@{} + foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) { + $pName = Get-Text $it "dcscor:parameter" + $val = $it.SelectSingleNode("dcscor:value", $ns) + if (-not $pName -or -not $val) { continue } + $rawVal = Read-AppearanceValueNode $val + $useV = Get-Text $it "dcscor:use" + $nestedItems = [ordered]@{} + foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) { + $subName = Get-Text $sub "dcscor:parameter" + $subVal = $sub.SelectSingleNode("dcscor:value", $ns) + if (-not $subName) { continue } + $subRaw = Read-AppearanceValueNode $subVal + $subUse = Get-Text $sub "dcscor:use" + $subEntry = [ordered]@{ value = $subRaw } + if ($subUse -eq 'false') { $subEntry['use'] = $false } + $nestedItems[$subName] = $subEntry + } + $valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line') + if ($valIsLine) { + if ($useV -eq 'false') { $rawVal['use'] = $false } + if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems } + $dict[$pName] = $rawVal + } elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) { + $wrap = [ordered]@{ value = $rawVal } + if ($useV -eq 'false') { $wrap['use'] = $false } + if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems } + $dict[$pName] = $wrap + } else { + $dict[$pName] = $rawVal + } + } + return $dict +} + +# Build conditionalAppearance array. +function Build-ConditionalAppearance { + param($caNode, [string]$loc) + if (-not $caNode) { return @() } + $out = @() + $i = 0 + foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) { + $entry = [ordered]@{} + $scopeNode = $it.SelectSingleNode("dcsset:scope", $ns) + if ($scopeNode -and $scopeNode.HasChildNodes) { + [Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)") + } + $selNode = $it.SelectSingleNode("dcsset:selection", $ns) + if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { + $entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection" + } + $filterNode = $it.SelectSingleNode("dcsset:filter", $ns) + if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) { + $f = @() + foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter") + if ($null -ne $bi) { $f += $bi } + } + $entry['filter'] = $f + } + $appNode = $it.SelectSingleNode("dcsset:appearance", $ns) + $ap = Get-SettingsAppearance $appNode + if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap } + $presNode = $it.SelectSingleNode("dcsset:presentation", $ns) + if ($presNode) { + $pres = Get-MLText $presNode + if (-not $pres) { $pres = $presNode.InnerText } + if ($pres) { $entry['presentation'] = $pres } + } + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + if ($vmN) { $entry['viewMode'] = $vmN.InnerText } + $usid = Get-Text $it "dcsset:userSettingID" + if ($usid) { $entry['userSettingID'] = 'auto' } + $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($uspN) { + $usp = Get-PresText $uspN + if ($usp) { $entry['userSettingPresentation'] = $usp } + } + $useV = Get-Text $it "dcsset:use" + if ($useV -eq 'false') { $entry['use'] = $false } + $useInDontUse = @() + foreach ($ch in $it.ChildNodes) { + if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue } + if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') { + $shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1) + $useInDontUse += $shortName + } + } + if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse } + $out += $entry + $i++ + } + return ,$out +} + +# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора). +# Вызывается один раз для любого элемента. Height тут — высота элемента (<Height>), +# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows. +function Add-Layout { + param($obj, $node) + # Общие свойства элемента (любой тип): default/drag/skip + if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true } + $soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') } + # EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet) + $esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') } + $edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') } + $fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm } + # AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault). + # Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики). + $amwNode = Get-Child $node 'AutoMaxWidth' + if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false } + elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true } + elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true } + $mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw } + if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false } + $mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh } + $w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w } + $h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h } + # Stretch: захват фактического значения (true И false — платформа эмитит явное) + $hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') } + $vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') } + $gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha } + $gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva } + $ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha } + # Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть» + foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') } + } + $fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha } + $hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha } +} + +# TitleLocation у check/radio (зеркало Emit-TitleLocation): +# тега нет → "" (дефолт платформы); значение = умный дефолт → опускаем; иначе пишем. +function Add-TitleLocation { + param($obj, $node, [string]$smartDefault) + $tl = Get-Child $node 'TitleLocation' + if ($null -eq $tl) { $obj['titleLocation'] = '' } + elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() } +} + +# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика } +# в порядке документа. Имена обработчиков всегда явные (как у событий формы) — +# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим. +function Get-Events { + param($node, [string]$elName) + $ev = $node.SelectSingleNode("lf:Events", $ns) + if (-not $ev) { return $null } + $events = [ordered]@{} + foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) { + $events[$e.GetAttribute("name")] = $e.InnerText + } + if ($events.Count -eq 0) { return $null } + return $events +} + +# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use). +# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }. +# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует. +function Decompile-XrFlag { + param($node, [string]$tag) + $el = $node.SelectSingleNode("*[local-name()='$tag']") + if (-not $el) { return $null } + $commonNode = $el.SelectSingleNode("*[local-name()='Common']") + $common = ($commonNode -and $commonNode.InnerText -eq 'true') + $valNodes = @($el.SelectNodes("*[local-name()='Value']")) + if ($valNodes.Count -eq 0) { return $common } + $roles = [ordered]@{} + foreach ($v in $valNodes) { + $rn = $v.GetAttribute("name") + if ($rn -match '^Role\.') { $rn = $rn.Substring(5) } + $roles[$rn] = ($v.InnerText -eq 'true') + } + $o = [ordered]@{} + $o['common'] = $common + $o['roles'] = $roles + return $o +} + +# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel, +# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки). +# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index + +# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible). +# Голый элемент (только команда, Type=Auto) → строковый shorthand. +function Decompile-CommandInterface { + $ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns) + if (-not $ciNode) { return $null } + $ci = [ordered]@{} + foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) { + $pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns) + if (-not $pn) { continue } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) { + $o = [ordered]@{} + $cmd = Get-Child $it 'Command' + $o['command'] = "$cmd" + $ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty } + $at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at } + $cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg } + $idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx } + $dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') } + $vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis } + # Голый элемент (только command) → строка-shorthand; иначе объект + if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) } + } + if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) } + } + if ($ci.Count -gt 0) { return $ci } + return $null +} + +# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть). +function Decompile-FunctionalOptions { + param($node) + $foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns) + if (-not $foNode) { return $null } + $opts = New-Object System.Collections.ArrayList + foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) { + $t = $it.InnerText.Trim() -replace '^FunctionalOption\.', '' + [void]$opts.Add($t) + } + if ($opts.Count -gt 0) { return ,@($opts) } + return $null +} + +# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions. +function Decompile-AttrColumn { + param($c) + $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") + $cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty } + $ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangText $ctNode; if ($null -ne $t) { $co['title'] = $t } } + $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } + # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита (bool | {common,roles}) + $cv = Decompile-XrFlag $c 'View'; if ($null -ne $cv) { $co['view'] = $cv } + $ce = Decompile-XrFlag $c 'Edit'; if ($null -ne $ce) { $co['edit'] = $ce } + return $co +} + +# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash +# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). +# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false. +# Источник: <xr:Ref> (именованная/стилевая) ИЛИ <xr:Abs> (встроенная → префикс "abs:"). +# Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект +# {src, loadTransparent?, transparentPixel?}. +function Get-PictureRef { + param($node, [string]$picTag) + $ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns) + $abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns) + if (-not $ref -and -not $abs) { return $null } + $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } + $lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns) + $ltTrue = ($lt -and $lt.InnerText -eq 'true') + $tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns) + if (-not $ltTrue -and -not $tpx) { return $src } + $o = [ordered]@{ src = $src } + if ($ltTrue) { $o['loadTransparent'] = $true } + if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } + return $o +} + +# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values): +# фиксируем только отклонение false. Источник <xr:Ref> или <xr:Abs> (→ "abs:"). При наличии +# <xr:TransparentPixel> → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture +# + отдельный loadTransparent:false. +function Set-CommandPicture { + param($obj, $node) + $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) + $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) + if (-not $ref -and -not $abs) { return } + $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } + $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns) + $ltFalse = ($lt -and $lt.InnerText -eq 'false') + $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) + if ($tpx) { + $o = [ordered]@{ src = $src } + if ($ltFalse) { $o['loadTransparent'] = $false } + $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } + $obj['picture'] = $o + } else { + $obj['picture'] = $src + if ($ltFalse) { $obj['loadTransparent'] = $false } + } +} + +# Шрифт <Font ...> → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты. +function Build-FontValue { + param($f) + $present = @() + foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + if ($f.HasAttribute($a)) { $present += $a } + } + # Чистый style-ref (ref + kind=StyleItem) → строка-шорткат + if ($present.Count -eq 2 -and ($present -contains 'ref') -and $f.GetAttribute('kind') -eq 'StyleItem') { + return $f.GetAttribute('ref') + } + $o = [ordered]@{} + foreach ($k in $present) { + $v = $f.GetAttribute($k) + if ($k -in @('height','scale') -and $v -match '^-?\d+$') { $o[$k] = [int]$v } + elseif ($k -in @('bold','italic','underline','strikeout')) { $o[$k] = ($v -eq 'true') } + else { $o[$k] = $v } + } + return $o +} + +# Граница <Border> → строка-ref (из стиля) или объект {width, style}. +function Build-BorderValue { + param($b) + if ($b.HasAttribute('ref')) { return $b.GetAttribute('ref') } + $o = [ordered]@{} + if ($b.HasAttribute('width')) { $w = $b.GetAttribute('width'); $o['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } } + $st = $b.SelectSingleNode("v8ui:style", $ns) + if ($st) { $o['style'] = $st.InnerText } + return $o +} + +# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка. +function Add-Appearance { + param($obj, $node) + $colorMap = @{ + 'TitleTextColor'='titleTextColor'; 'TitleBackColor'='titleBackColor' + 'FooterTextColor'='footerTextColor'; 'FooterBackColor'='footerBackColor' + 'TextColor'='textColor'; 'BackColor'='backColor'; 'BorderColor'='borderColor' + } + foreach ($tag in $colorMap.Keys) { + $c = $node.SelectSingleNode("lf:$tag", $ns) + if ($c) { $obj[$colorMap[$tag]] = $c.InnerText } + } + foreach ($pair in @(@('Font','font'), @('TitleFont','titleFont'), @('FooterFont','footerFont'))) { + $f = $node.SelectSingleNode("lf:$($pair[0])", $ns) + if ($f) { $obj[$pair[1]] = (Build-FontValue $f) } + } + $b = $node.SelectSingleNode("lf:Border", $ns) + if ($b) { $obj['border'] = (Build-BorderValue $b) } +} + +function Add-CommonProps { + param($obj, $node, [string]$elName) + Add-Appearance $obj $node + if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true } + if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true } + if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true } + $uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv } + $titleNode = $node.SelectSingleNode("lf:Title", $ns) + if ($titleNode) { + $t = Get-LangTextWS $titleNode # восстановление значимого пробела (whitespace-заголовок) + if ($null -ne $t) { $obj['title'] = $t } + # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) + } + $ttNode = $node.SelectSingleNode("lf:ToolTip", $ns) + if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } } + $ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr } + # Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon) + $hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp } + $fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp } + $ev = Get-Events $node $elName + if ($ev) { $obj['events'] = $ev } + # CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture): + # список отключённых команд редактора. Только <ExcludedCommand>, пустого не бывает. + $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) + if ($csNode) { + $exc = New-Object System.Collections.ArrayList + foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } + if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } + } +} + +# --- 3. Type decompile (inverse of Emit-Type) --- +function Decompile-Type { + param($typeNode) + if (-not $typeNode) { return $null } + $parts = New-Object System.Collections.ArrayList + foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) { + $raw = $vt.InnerText.Trim() + $short = $raw + # break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList). + switch -regex ($raw) { + '^xs:string$' { + $len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) + $al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns) + $fixed = ($al -and $al.InnerText -eq 'Fixed') # Variable = дефолт (опускаем); Fixed — явно + if ($len -and [int]$len.InnerText -gt 0) { + $short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" } + } else { $short = "string" } # Length=0 → всегда Variable (корпус) + break + } + '^xs:decimal$' { + $d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) + $f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) + $sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns) + $dd = if ($d) { $d.InnerText } else { '0' } + $ff = if ($f) { $f.InnerText } else { '0' } + if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" } + break + } + '^xs:boolean$' { $short = "boolean"; break } + '^xs:dateTime$' { + $df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) + $dfv = if ($df) { $df.InnerText } else { 'DateTime' } + switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } } + break + } + '^cfg:(.+)$' { $short = $matches[1]; break } + '^(v8|v8ui):' { + # Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом + # (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост. + $rev = @{ + 'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList' + 'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal' + 'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure' + 'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font' + } + if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw } + break + } + default { $short = $raw } + } + [void]$parts.Add($short) + } + # TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида». + # Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена. + foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) { + $raw = $ts.InnerText.Trim() + $short = $raw -replace '^(v8ui|v8|cfg):', '' + [void]$parts.Add($short) + } + # TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так + # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально + # стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID). + foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) { + [void]$parts.Add("typeid:" + $ti.InnerText.Trim()) + } + if ($parts.Count -eq 0) { return $null } + if ($parts.Count -eq 1) { return $parts[0] } + return ($parts -join ' | ') +} + +# Schema-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии +# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и +# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter. +function Build-DLInputParameters { + param($ipNode) + $items = New-Object System.Collections.ArrayList + foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) { + $io = [ordered]@{} + $io['parameter'] = Get-Child $it 'parameter' + $useN = $it.SelectSingleNode("dcscor:use", $ns) + if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false } + $valN = $it.SelectSingleNode("dcscor:value", $ns) + if ($valN) { + $vt = $valN.GetAttribute("type", $NS_XSI) + if ($vt -match 'ChoiceParameters$') { + $cps = New-Object System.Collections.ArrayList + foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { + $cpo = [ordered]@{} + $cpo['name'] = Get-Child $cpi 'choiceParameter' + $vals = New-Object System.Collections.ArrayList + foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) { + [void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI)))) + } + $cpo['values'] = @($vals) + [void]$cps.Add($cpo) + } + $io['choiceParameters'] = @($cps) + } elseif ($vt -match 'ChoiceParameterLinks$') { + $cpls = New-Object System.Collections.ArrayList + foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) { + $cpo = [ordered]@{} + $cpo['name'] = Get-Child $cpi 'choiceParameter' + $cpo['value'] = Get-Child $cpi 'value' + $md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md } + [void]$cpls.Add($cpo) + } + $io['choiceParameterLinks'] = @($cpls) + } else { + if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt } + } + } + [void]$items.Add($io) + } + return @($items) +} + +# dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного +# значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально, +# в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level). +function Build-FormDataParameters { + param($dpNode) + $entries = @() + foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) { + $pn = Get-Text $it "dcscor:parameter" + $use = Get-Text $it "dcscor:use" + $valNode = $it.SelectSingleNode("dcscor:value", $ns) + $usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns) + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($valNode -or $usidN -or $vmN -or $uspN) { + $obj = [ordered]@{ parameter = $pn } + if ($valNode) { + if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true } + else { + $vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText + if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal } + elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') } + else { $obj['value'] = $vVal } + if ($vType) { $obj['valueType'] = $vType } + } + } + if ($use -eq 'false') { $obj['use'] = $false } + if ($usidN) { $obj['userSettingID'] = 'auto' } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } } + $entries += $obj + } else { + $s = $pn; if ($use -eq 'false') { $s += ' @off' } + $entries += $s + } + } + return ,$entries +} + +function Build-DLParameter { + param($pNode) + $name = Get-Child $pNode 'name' + $o = [ordered]@{} + $o['name'] = $name + # title — опускаем, если совпадает с авто-выводом из имени (ru-only) + $titleNode = $pNode.SelectSingleNode("dcssch:title", $ns) + if ($titleNode) { + $t = Get-LangText $titleNode + if ($null -ne $t) { + $auto = Title-FromName -name $name + if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t } + } + } + # valueType + $vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns) + $typeVal = $null + if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } } + # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет <value xsi:nil/> + # не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит → + # явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.) + $vNode = $pNode.SelectSingleNode("dcssch:value", $ns) + if ($vNode -and ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true')) { + $o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI)) + } elseif ($vNode -and ((Get-Child $pNode 'valueListAllowed') -eq 'true')) { + $o['value'] = $null + } + # useRestriction — опускаем true (дефолт), фиксируем false + if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false } + # expression + $expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr } + # availableValues + $avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns)) + if ($avNodes.Count -gt 0) { + $avs = New-Object System.Collections.ArrayList + foreach ($avn in $avNodes) { + $avo = [ordered]@{} + $avv = $avn.SelectSingleNode("dcssch:value", $ns) + if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) } + else { $avo['value'] = $null } + $avp = $avn.SelectSingleNode("dcssch:presentation", $ns) + if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } } + [void]$avs.Add($avo) + } + $o['availableValues'] = @($avs) + } + # valueListAllowed / availableAsField + if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true } + if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false } + # inputParameters + $ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns) + if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } } + # denyIncompleteValues / use + if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true } + $use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use } + + # Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект. + $keys = @($o.Keys) + if ($keys.Count -eq 1) { return $name } + if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) } + return $o +} + +# --- 4. Element dispatch --- +$ELEMENT_KEY = @{ + 'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check'; + 'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField'; + 'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar'; + 'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup'; + 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl'; + 'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc'; + 'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar'; + 'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner'; + 'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart' +} + +# Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value. +$GENERIC_SCALARS = @( + @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } + @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } + @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } + @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } + @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } + @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } + @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } + @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } + @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } + @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } + @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } + @{ Tag='Mask'; Key='mask'; Kind='value' } + @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } + @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } + @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } + # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора) + @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } + @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } + @{ Tag='Output'; Key='output'; Kind='value' } + @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } + @{ Tag='PointerType'; Key='pointerType'; Kind='value' } + @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } + @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } + @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } + @{ Tag='Protection'; Key='protection'; Kind='bool' } + @{ Tag='Edit'; Key='edit'; Kind='bool' } + @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } + @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } + @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } + @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } + @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } + @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } + # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы + @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } + @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } + @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } + # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) + @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } + @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } + # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам + @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } + @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } + @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } + @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } + @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } + @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } + # Корпусный хвост (zеркало компилятора): свёртка группы / форма попапа / авто-добавление / + # выделение отрицательных / нач. позиция списка / высота списка выбора / три состояния / прокрутка + @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } + @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } + @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } + @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } + @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } + @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } + @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } + @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } + # Сочетание клавиш — общее свойство элемента (команда — отдельный путь) + @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } + # Батч простых скаляров (зеркало компилятора; Table HeaderHeight/FooterHeight/CurrentRowUse — отдельно) + @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } + @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } + @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } + @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } + @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } + @{ Tag='Shape'; Key='shape'; Kind='value' } + @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } + # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) + @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } + @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } +) + +# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает. +function Add-GenericScalars { + param($obj, $node) + foreach ($s in $GENERIC_SCALARS) { + if ($obj.Contains($s.Key)) { continue } + $v = Get-Child $node $s.Tag + if ($null -eq $v) { continue } + if ($s.Kind -eq 'bool') { $obj[$s.Key] = ($v -eq 'true') } else { $obj[$s.Key] = $v } + } +} + +function Decompile-Children { + param($parentNode, [string]$childContainer = 'ChildItems') + $container = $parentNode.SelectSingleNode("lf:$childContainer", $ns) + if (-not $container) { return $null } + $list = New-Object System.Collections.ArrayList + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + if ($COMPANION_TAGS -contains $child.LocalName) { continue } + $el = Decompile-Element $child + if ($el) { [void]$list.Add($el) } + } + if ($list.Count -eq 0) { return $null } + return ,@($list) +} + +# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом +# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing). +# Дин-список-таблица: компилятор-эвристика додумывает Autofill=false. Выражаем только ОТКЛОНЕНИЕ: +# autofill=false (нет детей) → совпадает с дефолтом → молчим; +# голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта). +function Decompile-CompanionPanel { + param($node, [string]$tag, [bool]$isDynListTable = $false) + $p = $node.SelectSingleNode("lf:$tag", $ns) + if (-not $p) { return $null } + $autofillRaw = Get-Child $p 'Autofill' + $halign = Get-Child $p 'HorizontalAlign' + $kids = Decompile-Children $p + $hasKids = $kids -and @($kids).Count -gt 0 + if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) { + if ($autofillRaw -eq 'false') { return $null } # = дефолт эвристики → молчим + return [ordered]@{ autofill = $true } # голая панель → отклонение + } + if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null } + $o = [ordered]@{} + if ($halign) { $o['horizontalAlign'] = $halign } + if ($autofillRaw -eq 'false') { $o['autofill'] = $false } + elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true } + if ($hasKids) { $o['children'] = $kids } + return $o +} + +# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null. +# У RadioButtonField и InputField. +function Decompile-ChoiceList { + param($node) + $cl = $node.SelectSingleNode("lf:ChoiceList", $ns) + if (-not $cl) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) { + $valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns) + $presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns) + $ci = [ordered]@{} + if ($valNode) { + $xsiType = $valNode.GetAttribute("type", $NS_XSI) + $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType + # Системное перечисление (ent:*) / иной не-примитивный, не-DesignTimeRef тип → сохраняем + # valueType (Normalize-ChoiceValue в компиляторе вывела бы xs:string и потеряла тип). + if ($xsiType -and $xsiType -notmatch '^xs:(string|decimal|boolean|dateTime)$' -and $xsiType -ne 'xr:DesignTimeRef') { + $ci['valueType'] = $xsiType + } + } + # Presentation: непустой → текст/мультиязык; пустой <Presentation/> → "" — суппресс-маркер, + # подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения). + if ($presNode) { + $p = Get-LangText $presNode + if ($null -ne $p -and $p -ne '') { $ci['presentation'] = $p } else { $ci['presentation'] = '' } + } + [void]$items.Add($ci) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо +# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value). +function Convert-ChoiceParamValue { + param($valNode) + $vt = $valNode.GetAttribute("type", $NS_XSI) + if ($vt -match 'FixedArray$') { + $arr = New-Object System.Collections.ArrayList + foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) { + $inner = $it.SelectSingleNode("lf:Value", $ns) + if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) } + } + return ,@($arr) + } + return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt +} + +# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}]. +function Decompile-ChoiceParameters { + param($node) + $cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns) + if (-not $cpn) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($cpn.SelectNodes("app:item", $ns))) { + $o = [ordered]@{} + $o['name'] = $it.GetAttribute("name") + $valNode = $it.SelectSingleNode("app:value/lf:Value", $ns) + if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode } + [void]$items.Add($o) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> → +# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит). +function Decompile-ChoiceParameterLinks { + param($node) + $cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns) + if (-not $cln) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) { + $o = [ordered]@{} + $o['name'] = Get-Text $lk "xr:Name" + $o['dataPath'] = Get-Text $lk "xr:DataPath" + $vc = Get-Text $lk "xr:ValueChange" + if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc } + [void]$items.Add($o) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}. +function Decompile-TypeLink { + param($node) + $tn = $node.SelectSingleNode("lf:TypeLink", $ns) + if (-not $tn) { return $null } + $o = [ordered]@{} + $o['dataPath'] = Get-Text $tn "xr:DataPath" + $li = Get-Text $tn "xr:LinkItem" + if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li } + return $o +} + +# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}). +function Add-FormatProps { + param($obj, $node) + $fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } } + $efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } } +} + +# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation. +# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно). +function Add-AdditionCore { + param($obj, $node, [string]$elName) + $src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText } + Add-CommonProps $obj $node $elName + $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } +} + +# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту +# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем. +function Decompile-TableAdditions { + param($tableNode, [string]$tableName) + $tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' } + $map = [ordered]@{} + foreach ($child in $tableNode.ChildNodes) { + if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + if (-not $tagToKey.ContainsKey($child.LocalName)) { continue } + $key = $tagToKey[$child.LocalName] + $nm = $child.GetAttribute("name") + $o = [ordered]@{}; $o[$key] = $nm + Add-AdditionCore $o $child $nm + Add-Layout $o $child + $o.Remove($key) # имя авто + if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') } # source=таблица дефолт + if ($o.Count -gt 0) { $map[$key] = $o } + } + if ($map.Count -gt 0) { return $map } + return $null +} + +# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode). +# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост. +function Decompile-SimpleField { + param($obj, $node, [string]$name, [string]$key) + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } +} + +# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField). +function Add-GaugeScalars { + param($obj, $node, $tags) + foreach ($p in $tags) { + $v = Get-Child $node $p + if ($null -eq $v) { continue } + $key = $p.Substring(0,1).ToLower() + $p.Substring(1) + if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v } + } +} + +function Decompile-Element { + param($node) + $tag = $node.LocalName + if (-not $ELEMENT_KEY.ContainsKey($tag)) { + Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag" + } + $key = $ELEMENT_KEY[$tag] + $name = $node.GetAttribute("name") + $obj = [ordered]@{} + + switch ($tag) { + 'UsualGroup' { + # group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем). + # group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление + # не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical' + # — иначе компилятор додумает <Group>Vertical</Group> там, где его нет. + $g = Get-Child $node 'Group' + $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } + if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } + $behavior = Get-Child $node 'Behavior' + if ($behavior) { + $bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' } + if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior } + } + $obj['name'] = $name + Add-CommonProps $obj $node $name + $rep = Get-Child $node 'Representation' + if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } } + $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) + $crt = $node.SelectSingleNode("lf:CollapsedRepresentationTitle", $ns); if ($crt) { $ct = Get-LangText $crt; if ($null -ne $ct -and $ct -ne '') { $obj['collapsedTitle'] = $ct } } + if ((Get-Child $node 'United') -eq 'false') { $obj['united'] = $false } + if ((Get-Child $node 'Collapsed') -eq 'true') { $obj['collapsed'] = $true } + # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы) + Add-FormatProps $obj $node + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'ColumnGroup' { + # columnGroup = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление + # не эмитим). Иначе компилятор додумает <Group>Horizontal</Group> там, где его нет. + $g = Get-Child $node 'Group' + $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'InCell'='inCell' } + if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } + $obj['name'] = $name + Add-CommonProps $obj $node $name + $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) + $sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'InputField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + # MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true») + $mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') } + # PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true») + $pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') } + $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangText $ih; if ($t) { $obj['inputHint'] = $t } } + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangText $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } + foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton','ChoiceListButton')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } + } + # InputField-специфичные bool-скаляры (захват «как есть») + foreach ($p in @('Wrap','OpenButton','ListChoiceMode','ExtendedEditMultipleValues','ChooseType','QuickChoice','AutoChoiceIncomplete')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) } + } + # InputField-специфичные value-скаляры (захват «как есть») + foreach ($p in @('ChoiceForm','ChoiceHistoryOnInput','ChoiceFoldersAndItems','FooterDataPath')) { + $v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = $v } + } + # MinValue/MaxValue — типизированное (<MinValue xsi:type="xs:decimal">N). Тип кодируем в JSON-тип: + # xs:decimal/int → число (компилятор → xs:decimal), иначе → строка (компилятор → xs:string). + foreach ($p in @('MinValue','MaxValue')) { + $mn = $node.SelectSingleNode("lf:$p", $ns) + if ($mn) { + $xt = $mn.GetAttribute("type", $NS_XSI); $txt = $mn.InnerText + $key = $p.Substring(0,1).ToLower() + $p.Substring(1) + if ($xt -match 'decimal|int') { + if ($txt -match '^-?\d+$') { $obj[$key] = [int]$txt } elseif ($txt -match '^-?\d+\.\d+$') { $obj[$key] = [decimal]$txt } else { $obj[$key] = $txt } + } else { $obj[$key] = $txt } + } + } + # Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор. + # availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type. + $tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) } + $atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } } + $cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr } + $cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp } + if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false } + $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } + Add-FormatProps $obj $node + # Параметры выбора / Связи параметров выбора / Связь по типу + $cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp } + $cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl } + $tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk } + } + 'CheckBoxField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + # CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение + $cbt = Get-Child $node 'CheckBoxType' + if ($null -eq $cbt) { $obj['checkBoxType'] = '' } + elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) } + Add-TitleLocation $obj $node 'Right' + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + Add-FormatProps $obj $node + } + 'RadioButtonField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + Add-TitleLocation $obj $node 'None' + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } + $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } + } + 'LabelDecoration' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } + # title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip) + $tiNode = $node.SelectSingleNode("lf:Title", $ns) + if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } + } + 'LabelField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + # LabelField: тег <Hiperlink> (опечатка платформы), не <Hyperlink> + if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true } + # PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения + $pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') } + $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } + # FooterDataPath / FooterText — общие cell-свойства колонки (как у input), не только input + $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } + $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangText $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } + Add-FormatProps $obj $node + } + 'PictureDecoration' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + # title декорации — единая ML-text форма с formatted (атрибут <Title formatted> у PictureDecoration) + $tiNode = $node.SelectSingleNode("lf:Title", $ns) + if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } + $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangText $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } + $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) + $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) + if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" } # встроенная картинка → префикс abs: + $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true } + # Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности + $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) + if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } + if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } + } + 'PictureField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } + $vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp } + $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangText $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } + } + 'CalendarField' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm } + $scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') } + $wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim } + $him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him } + $smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') } + } + 'Table' { + $obj[$key] = $name + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') } + $cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') } + if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true } + # enableDrag — теперь общий (Add-Layout, фактическое значение) + if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null } + if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false } + if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true } + # Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout) + $htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr } + # Высота шапки/подвала таблицы в строках (pass-through; компилятор эмитит в Emit-Table) + $hh = Get-Child $node 'HeaderHeight'; if ($null -ne $hh) { $obj['headerHeight'] = [int]$hh } + $fh = Get-Child $node 'FooterHeight'; if ($null -ne $fh) { $obj['footerHeight'] = [int]$fh } + # Использование текущей строки (Table-уровень; ≠ command-level CurrentRowUse) — pass-through + $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } + # Запрос обновления дин-списка (всегда PullFromTop в корпусе) — pass-through + $rr = Get-Child $node 'RefreshRequest'; if ($rr) { $obj['refreshRequest'] = $rr } + # CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем + # (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват). + $cbl = Get-Child $node 'CommandBarLocation' + if (Has-Child $node 'UpdateOnDataChange') { + if ($null -eq $cbl) { $obj['commandBarLocation'] = '' } + elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl } + } elseif ($cbl) { $obj['commandBarLocation'] = $cbl } + $ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl } + $vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl } + $scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl } + # --- Общие свойства таблицы (любой тип таблицы, не только динсписок) --- + if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true } + $selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm } + $rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm } + if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false } + if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false } + if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true } + # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill). + $taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') } + if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true } + $soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin } + $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } + $itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv } + # RowsPicture — конвенция ValuesPicture (Ref/Abs + LoadTransparent дефолт false + TransparentPixel) + $rp = Get-PictureRef $node 'RowsPicture'; if ($null -ne $rp) { $obj['rowsPicture'] = $rp } + $rpdp = Get-Child $node 'RowPictureDataPath' + # --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) --- + if (Has-Child $node 'UpdateOnDataChange') { + if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true } + $arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp } + $cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi } + if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true } + if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false } + if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true } + $uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc } + if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false } + # RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture + if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' } + elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp } + $usg = Get-Child $node 'UserSettingsGroup'; if ($usg) { $obj['userSettingsGroup'] = $usg } + } elseif ($rpdp) { $obj['rowPictureDataPath'] = $rpdp } + $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) + if ($csNode) { + $exc = New-Object System.Collections.ArrayList + foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } + if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } + } + $cols = Decompile-Children $node + if ($cols) { $obj['columns'] = $cols } + # Стандартные дополнения уровня таблицы (прямые дети) → карта отклонений additions + $addMap = Decompile-TableAdditions $node $name + if ($addMap) { $obj['additions'] = $addMap } + } + 'Pages' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $pr = Get-Child $node 'PagesRepresentation'; if ($pr) { $obj['pagesRepresentation'] = $pr } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'Page' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $g = Get-Child $node 'Group' + $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } + if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] } + # Картинка страницы (иконка вкладки) — конвенция ValuesPicture (дефолт LoadTransparent=false) + $pp = Get-PictureRef $node 'Picture'; if ($null -ne $pp) { $obj['picture'] = $pp } + $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) + # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы) + Add-FormatProps $obj $node + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'Button' { + $obj[$key] = $name + $cmd = Get-Child $node 'CommandName' + if ($cmd) { + if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] } + elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] } + elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" } + else { $obj['commandName'] = $cmd } + } + $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } + Add-CommonProps $obj $node $name + $type = Get-Child $node 'Type' + if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } } + if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true } + if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true } + Set-CommandPicture $obj $node + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic } + } + 'ButtonGroup' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $cs = Get-Child $node 'CommandSource'; if ($cs) { $obj['commandSource'] = $cs } + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'CommandBar' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + $cs = Get-Child $node 'CommandSource'; if ($cs) { $obj['commandSource'] = $cs } + $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } + if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'Popup' { + $obj[$key] = $name + Add-CommonProps $obj $node $name + Set-CommandPicture $obj $node + $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } + $kids = Decompile-Children $node + if ($kids) { $obj['children'] = $kids } + } + 'SearchStringAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } + 'ViewStatusAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } + 'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } + 'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'HTMLDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'TextDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'FormattedDocumentField' { Decompile-SimpleField $obj $node $name $key } + 'ProgressBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') } + 'TrackBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') } + 'ChartField' { Decompile-SimpleField $obj $node $name $key } + 'GraphicalSchemaField' { Decompile-SimpleField $obj $node $name $key } + 'PlannerField' { Decompile-SimpleField $obj $node $name $key } + 'PeriodField' { Decompile-SimpleField $obj $node $name $key } + 'DendrogramField' { Decompile-SimpleField $obj $node $name $key } + 'GanttChartField' { + Decompile-SimpleField $obj $node $name $key + # Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element. + # Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере). + $tblNode = $node.SelectSingleNode("lf:Table", $ns) + if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode } + } + } + # DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть». + $di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di } + # title: "" — подавление авто-вывода: для типов, где компилятор вывел бы + # заголовок из имени, а в оригинале <Title> отсутствует. + if (-not $obj.Contains('title')) { + $autoTitle = $false + if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true } + elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) } + elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') } + if ($autoTitle) { $obj['title'] = '' } + } + Add-Layout $obj $node + Add-GenericScalars $obj $node + # extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) → + # строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink/events) → объект { text?, … }. + $etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns) + if ($etNode) { + $etTitle = $etNode.SelectSingleNode("lf:Title", $ns) + $textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null } + $etObj = [ordered]@{} + Add-Layout $etObj $etNode + Add-GenericScalars $etObj $etNode + Add-Appearance $etObj $etNode + if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true } + if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true } + if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true } + # События компаньона (напр. URLProcessing у hyperlink-подсказки) — переиспользуем механизм событий элемента + $etEv = Get-Events $etNode $name; if ($etEv) { $etObj['events'] = $etEv } + if ($etObj.Count -gt 0) { + if ($null -ne $textVal) { + if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } } + else { $etObj['text'] = $textVal } + # formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте + if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true } + } + $obj['extendedTooltip'] = $etObj + } elseif ($null -ne $textVal) { + $obj['extendedTooltip'] = $textVal + } + } + # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент) + $isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange') + $cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable + if ($null -ne $cb) { $obj['commandBar'] = $cb } + $cm = Decompile-CompanionPanel $node 'ContextMenu' + if ($null -ne $cm) { $obj['contextMenu'] = $cm } + return $obj +} + +# ───────────────────────────────────────────────────────────────────────────── +# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите. +# Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings. +function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') } +function PLD-Int { param($v) if ($null -eq $v) { return $null } return [int]$v } +# <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null. +function Get-PlannerValue { + param($node) + if (-not $node) { return $null } + if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null } + if ($node.InnerText) { return $node.InnerText } else { return $null } +} +function Build-PlannerFont { + param($node) + if (-not $node) { return $null } + $o = [ordered]@{} + foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { + $av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av } + } + if ($o.Count -eq 0) { return $null } + return $o +} +function Build-PlannerBorder { + param($node) + if (-not $node) { return $null } + $o = [ordered]@{} + $w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } + $st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText } + return $o +} +function Build-PlannerItem { + param($itn) + $o = [ordered]@{} + $valNode = $itn.SelectSingleNode("*[local-name()='value']") + if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText } + $o['text'] = (Get-Child $itn 'text') + $tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt } + $o['begin'] = (Get-Child $itn 'begin') + $o['end'] = (Get-Child $itn 'end') + $o['borderColor'] = (Get-Child $itn 'borderColor') + $o['backColor'] = (Get-Child $itn 'backColor') + $o['textColor'] = (Get-Child $itn 'textColor') + $fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } + $o['replacementDate'] = (Get-Child $itn 'replacementDate') + $o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted')) + $o['id'] = (Get-Child $itn 'id') + $o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted')) + $brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd } + $o['editMode'] = (Get-Child $itn 'editMode') + return $o +} +function Build-PlannerDimElement { + param($eln) + $o = [ordered]@{} + $v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } + $o['text'] = (Get-Child $eln 'text') + $o['borderColor'] = (Get-Child $eln 'borderColor') + $o['backColor'] = (Get-Child $eln 'backColor') + $o['textColor'] = (Get-Child $eln 'textColor') + $fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } + $subs = New-Object System.Collections.ArrayList + foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) } + if ($subs.Count -gt 0) { $o['elements'] = @($subs) } + $sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') } + $o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted')) + return $o +} +function Build-PlannerDimension { + param($dn) + $o = [ordered]@{} + $v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } + $o['text'] = (Get-Child $dn 'text') + $o['borderColor'] = (Get-Child $dn 'borderColor') + $o['backColor'] = (Get-Child $dn 'backColor') + $o['textColor'] = (Get-Child $dn 'textColor') + $fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } + $els = New-Object System.Collections.ArrayList + foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) } + if ($els.Count -gt 0) { $o['elements'] = @($els) } + $o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted')) + return $o +} +function Build-PlannerLevel { + param($lvn) + $o = [ordered]@{} + $o['measure'] = (Get-Child $lvn 'measure') + $o['interval'] = (PLD-Int (Get-Child $lvn 'interval')) + $o['show'] = (PLD-Bool (Get-Child $lvn 'show')) + $lineNode = $lvn.SelectSingleNode("*[local-name()='line']") + if ($lineNode) { + $ln = [ordered]@{} + $w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w } + $g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') } + $st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText } + $o['line'] = $ln + } + $o['scaleColor'] = (Get-Child $lvn 'scaleColor') + $o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule') + $fmtNode = $lvn.SelectSingleNode("*[local-name()='format']") + if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } } + $labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']") + if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } } + $o['backColor'] = (Get-Child $lvn 'backColor') + $o['textColor'] = (Get-Child $lvn 'textColor') + $o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels')) + return $o +} +function Build-PlannerTimeScale { + param($tsn) + $o = [ordered]@{} + $o['placement'] = (Get-Child $tsn 'placement') + $levels = New-Object System.Collections.ArrayList + foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) } + $o['levels'] = @($levels) + $o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent')) + $o['backColor'] = (Get-Child $tsn 'backColor') + $o['textColor'] = (Get-Child $tsn 'textColor') + $o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel')) + return $o +} +function Build-PlannerSettings { + param($setNode) + $pl = [ordered]@{} + $itemNodes = @($setNode.SelectNodes("*[local-name()='item']")) + if ($itemNodes.Count -gt 0) { + $items = New-Object System.Collections.ArrayList + foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) } + $pl['items'] = @($items) + } + $dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']")) + if ($dimNodes.Count -gt 0) { + $dims = New-Object System.Collections.ArrayList + foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) } + $pl['dimensions'] = @($dims) + } + $pl['borderColor'] = (Get-Child $setNode 'borderColor') + $pl['backColor'] = (Get-Child $setNode 'backColor') + $pl['textColor'] = (Get-Child $setNode 'textColor') + $pl['lineColor'] = (Get-Child $setNode 'lineColor') + $fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt } + $pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod') + $pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod') + $pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale')) + $pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders')) + $pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders')) + $wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']") + if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } } + $pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit') + $pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition')) + $pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent')) + $pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent')) + $tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']") + if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) } + $perNode = $setNode.SelectSingleNode("*[local-name()='period']") + if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } } + $pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate')) + $pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation') + $pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient') + $pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth')) + $pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight')) + $pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth')) + $pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight')) + $pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader') + $pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader') + $brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd } + $pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType') + return $pl +} + +# ───────────────────────────────────────────────────────────────────────────── +# Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок: +# рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии) +# детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML), +# серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип). +$CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } +$CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 } +$CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } +function Conv-ChartScalar { + param([string]$v) + if ($v -eq 'true') { return $true } + if ($v -eq 'false') { return $false } + return $v +} +function Build-ChartNode { + param($n, [string]$name) + # ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени) + if ($CHART_ML_FIELDS.Contains($name)) { + $ml = Get-LangText $n + if ($null -eq $ml) { return '' } else { return $ml } + } + $kids = @($n.SelectNodes("*")) + if ($kids.Count -eq 0) { + # лист: attrs-only (шрифт/gaugeQualityBands) или текст + $attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' }) + if ($attrs.Count -gt 0) { + $o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o + } + return (Conv-ChartScalar $n.InnerText) + } + # line/border: дочерний v8ui:style (+ width[/gap]) + $styleChild = $n.SelectSingleNode("*[local-name()='style']") + if ($styleChild) { + $o = [ordered]@{} + $w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } + $g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') } + $o['style'] = $styleChild.InnerText + return $o + } + # вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени + $o = [ordered]@{} + foreach ($c in $kids) { + $ln = $c.LocalName + $val = Build-ChartNode $c $ln + if ($CHART_SERIES_FIELDS.Contains($ln)) { + if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList } + [void]$o[$ln].Add($val) + } elseif ($o.Contains($ln)) { + if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp } + [void]$o[$ln].Add($val) + } else { + $o[$ln] = $val + } + } + # нормализуем ArrayList → @() для сериализации + foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } } + return $o +} +function Build-ChartSettings { + param($setNode) + return (Build-ChartNode $setNode '') +} + +# --- 5. Form-level assembly --- +$dsl = [ordered]@{} + +$titleNode = $root.SelectSingleNode("lf:Title", $ns) +if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } } + +# properties (прямые скаляры под <Form>, PascalCase → camelCase) +$KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance','ShowCloseButton','HorizontalAlign','ChildrenAlign','ShowTitle','ConversationsRepresentation','CollapseItemsByImportanceVariant','GroupList','ChildItemsWidth','VerticalAlign','HorizontalSpacing') +$props = [ordered]@{} +foreach ($pn in $KNOWN_FORM_PROPS) { + $v = Get-Child $root $pn + if ($null -ne $v) { + $camel = $pn.Substring(0,1).ToLower() + $pn.Substring(1) + if ($v -eq 'true') { $props[$camel] = $true } + elseif ($v -eq 'false') { $props[$camel] = $false } + elseif ($v -match '^\d+$') { $props[$camel] = [int]$v } + else { $props[$camel] = $v } + } +} +# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим: +# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title); +# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить). +if ($dsl.Contains('title')) { + if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' } + elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') } +} +if ($props.Count -gt 0) { $dsl['properties'] = $props } + +# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок +$mdcb = $root.SelectSingleNode("lf:MobileDeviceCommandBarContent", $ns) +if ($mdcb) { + $names = New-Object System.Collections.ArrayList + foreach ($it in @($mdcb.SelectNodes("xr:Item", $ns))) { + $v = $it.SelectSingleNode("xr:Value", $ns); if ($v) { [void]$names.Add($v.InnerText) } + } + if ($names.Count -gt 0) { $dsl['mobileCommandBarContent'] = @($names) } +} + +# excludedCommands (form-level <CommandSet>) +$csForm = $root.SelectSingleNode("lf:CommandSet", $ns) +if ($csForm) { + $excForm = New-Object System.Collections.ArrayList + foreach ($ec in @($csForm.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$excForm.Add($ec.InnerText) } + if ($excForm.Count -gt 0) { $dsl['excludedCommands'] = @($excForm) } +} + +# events (form-level) +$evForm = Get-Events $root $null +if ($evForm) { + # form-level: компилятор хранит как {Event: handler} напрямую + $evMap = [ordered]@{} + $evNode = $root.SelectSingleNode("lf:Events", $ns) + foreach ($e in @($evNode.SelectNodes("lf:Event", $ns))) { $evMap[$e.GetAttribute("name")] = $e.InnerText } + if ($evMap.Count -gt 0) { $dsl['events'] = $evMap } +} + +# Зеркало компилятор-эвристики B3 (Compute-MainAcbAutofill): наличие cmdBar-элемента где-либо +# в дереве → форменный AutoCommandBar autofill=false. Нужно, чтобы предсказать вывод и выразить +# отклонение (голый корень при наличии cmdBar). +function Test-AnyCmdBar { + param($list) + if (-not $list) { return $false } + foreach ($e in $list) { + if ($e -is [System.Collections.IDictionary] -and $e.Contains('cmdBar')) { return $true } + if ($e -is [System.Collections.IDictionary]) { + if ($e.Contains('children') -and (Test-AnyCmdBar $e['children'])) { return $true } + if ($e.Contains('columns') -and (Test-AnyCmdBar $e['columns'])) { return $true } + } + } + return $false +} + +# elements (+ форменный AutoCommandBar как autoCmdBar-элемент, если у него есть содержимое/отклонение) +$elemList = New-Object System.Collections.ArrayList +$elements = Decompile-Children $root +$formHasCmdBar = Test-AnyCmdBar $elements +$acb = $root.SelectSingleNode("lf:AutoCommandBar", $ns) +if ($acb) { + $haln = Get-Child $acb 'HorizontalAlign' + $acbAutofill = Get-Child $acb 'Autofill' + $acbKids = Decompile-Children $acb + $acbObj = $null + if ($haln -or ($acbAutofill -eq 'false') -or $acbKids) { + $acbObj = [ordered]@{} + $acbObj['autoCmdBar'] = $acb.GetAttribute("name") + if ($haln) { $acbObj['horizontalAlign'] = $haln } + if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false } + if ($acbKids) { $acbObj['children'] = $acbKids } + } elseif ($formHasCmdBar -and $null -eq $acbAutofill) { + # Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер + $acbObj = [ordered]@{} + $acbObj['autoCmdBar'] = $acb.GetAttribute("name") + $acbObj['autofill'] = $true + } + if ($acbObj) { [void]$elemList.Add($acbObj) } +} +if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } } +if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) } + +# attributes +# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3. +function Test-IsObjectLikeTypeDec([string]$type) { + if ([string]::IsNullOrEmpty($type)) { return $false } + if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true } + return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.') +} +$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns) +if ($attrsNode) { + $attrs = New-Object System.Collections.ArrayList + # Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно + # один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет) + # → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно. + $allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns)) + $anyMainAttr = $false; $objLikeNodes = @() + foreach ($an in $allAttrNodes) { + if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true } + $atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns)) + if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an } + } + $suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null } + foreach ($a in $allAttrNodes) { + $ao = [ordered]@{} + $ao['name'] = $a.GetAttribute("name") + $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } + # valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList + # (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно. + $setNode = $a.SelectSingleNode("lf:Settings", $ns) + if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') { + $vt = Decompile-Type $setNode + $ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер "" + } + # Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват). + elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') { + $ao['planner'] = Build-PlannerSettings $setNode + } + # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart"> → chart (генерик). + elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:(Gantt)?Chart$') { + $ao['chart'] = Build-ChartSettings $setNode + } + if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } + elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false } + $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } + $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } + # Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок + # из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем + # ключ (компилятор воспроизведёт); иначе → явный заголовок. + $isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title + $tNode = $a.SelectSingleNode("lf:Title", $ns) + if ($tNode) { + $t = Get-LangText $tNode + if ($null -ne $t) { + if ($isMain -or -not ($t -is [string]) -or $t -ne (Title-FromName $ao['name'])) { $ao['title'] = $t } + } + } elseif (-not $isMain) { + $ao['title'] = '' + } + # SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved: + # Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал + # тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute). + if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } + elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false } + # Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true; + # иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив. + $saveNode = $a.SelectSingleNode("lf:Save", $ns) + if ($saveNode) { + $nm = "$($ao['name'])" + $flds = @($saveNode.SelectNodes("lf:Field", $ns) | ForEach-Object { $_.InnerText }) + if ($flds.Count -eq 1 -and $flds[0] -eq $nm) { + $ao['save'] = $true + } elseif ($flds.Count -gt 0) { + $stripped = @($flds | ForEach-Object { + if ($_ -match "^$([regex]::Escape($nm))\.(.+)$") { $matches[1] } else { $_ } + }) + if ($stripped.Count -eq 1) { $ao['save'] = $stripped[0] } else { $ao['save'] = $stripped } + } + } + $fc = Get-Child $a 'FillCheck'; if ($fc) { $ao['fillCheck'] = $fc } + $afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo } + $colsNode = $a.SelectSingleNode("lf:Columns", $ns) + if ($colsNode) { + $cols = New-Object System.Collections.ArrayList + foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) { [void]$cols.Add((Decompile-AttrColumn $c)) } + if ($cols.Count -gt 0) { $ao['columns'] = @($cols) } + # AdditionalColumns: доп. колонки табличных частей объекта (группа на табличную часть) + $addNodes = @($colsNode.SelectNodes("lf:AdditionalColumns", $ns)) + if ($addNodes.Count -gt 0) { + $addList = New-Object System.Collections.ArrayList + foreach ($an in $addNodes) { + $acObj = [ordered]@{}; $acObj['table'] = $an.GetAttribute("table") + $acCols = New-Object System.Collections.ArrayList + foreach ($c in @($an.SelectNodes("lf:Column", $ns))) { [void]$acCols.Add((Decompile-AttrColumn $c)) } + $acObj['columns'] = @($acCols) + [void]$addList.Add($acObj) + } + $ao['additionalColumns'] = @($addList) + } + } + # UseAlways: поля, всегда читаемые. Префикс "ИмяРеквизита." снимаем. + # ValueTable (есть columns): useAlways:true на совпавшей колонке; остальные → массив атрибута. + # Дин-список/прочие (нет columns): массив useAlways на атрибуте. + $uaNode = $a.SelectSingleNode("lf:UseAlways", $ns) + if ($uaNode) { + $prefix = "$($ao['name'])." + $shorts = New-Object System.Collections.ArrayList + foreach ($fn in @($uaNode.SelectNodes("lf:Field", $ns))) { + $t = $fn.InnerText.Trim() + # Снимаем префикс "ИмяРеквизита.". Маркер "~" (query-поле дин-списка) сохраняем, + # префикс снимаем ПОСЛЕ него: ~Список.Остановлен → ~Остановлен (компилятор развернёт обратно). + if ($t.StartsWith('~')) { + $rest = $t.Substring(1) + if ($rest.StartsWith($prefix)) { $rest = $rest.Substring($prefix.Length) } + $t = "~$rest" + } elseif ($t.StartsWith($prefix)) { + $t = $t.Substring($prefix.Length) + } + [void]$shorts.Add($t) + } + if ($ao.Contains('columns')) { + $rest = New-Object System.Collections.ArrayList + foreach ($s in $shorts) { + $col = $ao['columns'] | Where-Object { $_['name'] -eq $s } | Select-Object -First 1 + if ($col) { $col['useAlways'] = $true } else { [void]$rest.Add($s) } + } + if ($rest.Count -gt 0) { $ao['useAlways'] = @($rest) } + } elseif ($shorts.Count -gt 0) { + $ao['useAlways'] = @($shorts) + } + } + # Settings динамического списка (только xsi:type=DynamicList; Planner/TypeDescription — выше) + $setNode = $a.SelectSingleNode("lf:Settings", $ns) + if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'DynamicList$') { + $so = [ordered]@{} + # AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть». + $afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') } + $mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt } + # AutoSaveUserSettings — авто-сохранение польз. настроек дин-списка (в корпусе только false; + # дефолт true → платформа эмитит отклонение). Захват факт. значения. + $asus = Get-Child $setNode 'AutoSaveUserSettings'; if ($null -ne $asus) { $so['autoSaveUserSettings'] = ($asus -eq 'true') } + $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns) + if ($qtNode -and $qtNode.InnerText) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" } + # DynamicDataRead: дефолт true → эмитим только false + if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false } + # Явные поля набора (редко, ~4.5%) — захват только при наличии Field + $fieldNodes = @($setNode.SelectNodes("lf:Field", $ns)) + if ($fieldNodes.Count -gt 0) { + $fields = New-Object System.Collections.ArrayList + foreach ($fn in $fieldNodes) { + $fo = [ordered]@{} + $fld = Get-Child $fn 'field' + $dp = Get-Child $fn 'dataPath' + if ($fld) { $fo['field'] = $fld } + if ($dp -and $dp -ne $fld) { $fo['dataPath'] = $dp } + # Тип поля набора: DataSetFieldField (дефолт, компилятор хардкодит) vs + # DataSetFieldNestedDataSet (поле-вложенный набор = реквизит табличной части). Маркер nested. + if ($fn.GetAttribute("type", $NS_XSI) -match 'NestedDataSet$') { $fo['nested'] = $true } + $ftn = $fn.SelectSingleNode("dcssch:title", $ns) + if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } } + [void]$fields.Add($fo) + } + $so['fields'] = @($fields) + } + # Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings) + $paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns)) + if ($paramNodes.Count -gt 0) { + $dlPars = New-Object System.Collections.ArrayList + foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) } + $so['parameters'] = @($dlPars) + } + # ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор + # регенерит каноничный скелет. Захватываем только контейнеры с реальными + # dcsset:item (filter/order/conditionalAppearance) в формат компилятора. + $lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns) + if ($lsNode) { + $fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns) + if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) { + $flt = @() + foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { + $bi = (Build-FilterItem -itemNode $fc -loc "settings/filter") + if ($null -ne $bi) { $flt += $bi } + } + if ($flt.Count -gt 0) { $so['filter'] = @($flt) } + } + $oNode = $lsNode.SelectSingleNode("dcsset:order", $ns) + if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) { + $ord = Build-Order -ordNode $oNode -loc "settings/order" + if (@($ord).Count -gt 0) { $so['order'] = @($ord) } + } + $caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns) + if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) { + $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance" + if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) } + } + # Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в + # настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters. + $dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns) + if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) { + $dp = Build-FormDataParameters $dpNode + if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) } + } + # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). + # Канон → $null (компилятор регенерит полный скелет, как раньше). + $lsShape = Get-ListSettingsShape $lsNode + if ($null -ne $lsShape) { $so['listSettings'] = $lsShape } + } + if ($so.Count -gt 0) { $ao['settings'] = $so } + } + [void]$attrs.Add($ao) + } + if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) } +} + +# conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>; +# та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance) +if ($attrsNode) { + $caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns) + if ($caNode) { + $ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance" + if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) } + } +} + +# parameters +$parsNode = $root.SelectSingleNode("lf:Parameters", $ns) +if ($parsNode) { + $pars = New-Object System.Collections.ArrayList + foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) { + $po = [ordered]@{}; $po['name'] = $p.GetAttribute("name") + $ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty } + if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true } + [void]$pars.Add($po) + } + if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) } +} + +# commands +$cmdsNode = $root.SelectSingleNode("lf:Commands", $ns) +if ($cmdsNode) { + $cmds = New-Object System.Collections.ArrayList + foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) { + $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") + $act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act } + if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true } + # Заголовок команды: есть <Title> → захват; нет → суппресс-маркер "" (иначе компилятор + # додумает из имени — авто-вывод неверен для ~0.13% команд без заголовка в оригинале). + $tNode = $c.SelectSingleNode("lf:Title", $ns) + if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } + else { $co['title'] = '' } + $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } + $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } + $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } + $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } + # Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>) + $ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate } + $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } + Set-CommandPicture $co $c + $rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep } + [void]$cmds.Add($co) + } + if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) } +} + +# commandInterface (форменный <CommandInterface> — последний дочерний Form) +$ci = Decompile-CommandInterface +if ($null -ne $ci) { $dsl['commandInterface'] = $ci } + +# --- 6. Output --- +$json = ConvertTo-CompactJson -obj $dsl +if ($OutputPath) { + [System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false))) + Save-QueryFiles + Write-Host "form-decompile: $OutputPath" +} else { + Write-Output $json +} diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 853eedb2..89f2ff72 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -940,7 +940,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и | `query` | string | Текст запроса (`ManualQuery=true`). Поддерживает `@file.sql` (путь относительно JSON) | | `dynamicDataRead` | bool | Динамическое считывание. **Умолчание `true`** — указывать только для отключения (`false`) | | `autoFillAvailableFields` | bool | Автозаполнение доступных полей (`<AutoFillAvailableFields>`). **Умолчание `true`** — указывать только для отключения (`false`; тогда поля берутся из явного запроса, не авто). Эмитится первым в `<Settings>` | -| `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title? }` — для переопределения заголовка. Обычно поля выводятся из запроса автоматически | +| `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title?, nested? }` — для переопределения заголовка. `nested: true` помечает поле-вложенный набор (`DataSetFieldNestedDataSet` = реквизит табличной части объекта; дефолт — `DataSetFieldField`). Обычно поля выводятся из запроса автоматически | | `parameters` | array | Параметры схемы запроса (`DataCompositionSchemaParameter`) — см. ниже | | `order` | array | Сортировка списка (см. ниже) | | `filter` | array | Отбор списка (грамматика как в СКД) | @@ -951,7 +951,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и Пустой блок настроек компоновщика (`ListSettings`) генерируется автоматически (каноничный полный скелет платформы — filter+order+conditionalAppearance+itemsViewMode+itemsUserSettingID, ~93% форм); указывать ничего не нужно. -| `listSettings` | object | **Дескриптор формы скелета `<ListSettings>`** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID` → `true`. Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Декомпилятор пишет дескриптор только для отклонений от канона | +| `listSettings` | object | **Дескриптор формы скелета `<ListSettings>`** — только для НЕ-каноничных (частичных/минимальных) форм. Ordered-карта present top-level элементов: контейнеры `filter`/`order`/`conditionalAppearance` → блок-мета (`"vu"`=viewMode+userSettingID, `"u"`=только userSettingID, `"v"`, `""`); `itemsViewMode`/`itemsUserSettingID` → `true`. Компилятор эмитит ТОЛЬКО указанные части (контент берёт из `filter`/`order`/`conditionalAppearance`). Нет ключа → полный каноничный скелет. Пустой объект `{}` → self-closing `<ListSettings/>` (оригинал без скелета). Декомпилятор пишет дескриптор только для отклонений от канона | #### parameters — параметры схемы дин-списка @@ -979,6 +979,8 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и Объектные ключи (как в СКД): `name`, `title`, `type`/`valueType`, `value`, `valueListAllowed`, `useRestriction`, `availableAsField`, `expression`, `availableValues` (`[{ value, presentation }]`), `inputParameters`, `denyIncompleteValues`, `use`. +> **Тип-токен `typeid:<GUID>`** (раундтрип, не для ручного авторинга) — тип, заданный глобальным стабильным GUID (`<v8:TypeId>`, не `<v8:Type>`). Платформа так сериализует типы, чьё имя в данном контексте недоступно (определяемые типы / характеристики). GUID глобально стабилен → эмитится verbatim. Применим везде, где принимается `type`/`valueType` (параметры, реквизиты). Декомпилятор ставит его сам; вручную указывают только реальный существующий GUID типа конфигурации. + > **`value: null` при `valueListAllowed: true`** — явный маркер «эмитить `<dcssch:value xsi:nil/>`». Платформа пишет nil-значение для valueListAllowed-параметра не всегда (корпус 27 с / 47 без); по умолчанию (ключ `value` отсутствует) компилятор его НЕ эмитит. Декомпилятор ставит `value: null`, когда оригинал содержит nil-тег. #### order / filter / conditionalAppearance