# form-decompile v0.81 — 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)> не
— это не управляемая форма.") 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 → вынести в `-.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 конфигурацией ( 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$') { Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -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 } # Авто-вывод заголовка из имени — ТОЧНОЕ зеркало 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 } $text = Get-LangText $titleNode if ($null -eq $text) { return $null } # Whitespace-only заголовок (декорация-разделитель): PreserveWhitespace=false стрипает значимый # пробел в → пустой текст при наличии узла content. Платформа НЕ # эмитит пустой Title, значит исходно был пробел — восстанавливаем (иначе спутаем с суппресс-маркером ""). if ($text -is [string] -and $text -eq '' -and $titleNode.SelectSingleNode("v8:item/v8:content", $ns)) { $text = ' ' } $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 } # Прочитать в JSON-значение: Font/Line/multilang/raw text. function Read-AppearanceValueNode { param($valNode) if (-not $valNode) { return $null } $vt = Get-LocalXsiType $valNode if ($vt -eq 'LocalStringType') { return (Get-MLText $valNode) } if ($vt -eq 'Font') { return (Get-FontValue $valNode) } if ($vt -eq 'Line') { return (Get-LineValue $valNode) } 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'; '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 } } $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 из (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 тут — высота элемента (), # в т.ч. у Table; высоту в строках () 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() } } # Разобрать элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика } # в порядке документа. Имена обработчиков всегда явные (как у событий формы) — # единый, консистентный с 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). # […] → скаляр 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 } # Командный интерфейс формы (): панели 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 } # FunctionalOption.X…> → массив строк (префикс 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 } return $co } # Общие свойства элемента (visible/enabled/readonly/title/events) → в hash # Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture). # Платформа ВСЕГДА эмитит (и true, и false) → дефолт DSL = false. # Скаляр (Ref) при loadTransparent=false; объект {src,loadTransparent:true} при true. function Get-PictureRef { param($node, [string]$picTag) $ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns) if (-not $ref) { return $null } $lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns) if ($lt -and $lt.InnerText -eq 'true') { return [ordered]@{ src = $ref.InnerText; loadTransparent = $true } } return $ref.InnerText } # Шрифт → строка-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 } # Граница → строка-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-LangText $titleNode if ($null -ne $t) { $obj['title'] = $t } # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) } $ttNode = $node.SelectSingleNode("lf:ToolTip", $ns) if ($ttNode) { $tt = Get-LangText $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 } } # --- 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) if ($len -and [int]$len.InnerText -gt 0) { $short = "string($($len.InnerText))" } else { $short = "string" } 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-параметры динамического списка ( под ) — зеркало эмиссии # 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) } 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 (дефолт) $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)) } # 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' } ) # Захват 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 (нет детей) → совпадает с дефолтом → молчим; # голый (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: … → [ { 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 } # 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 } # Значение Параметра выбора (): скаляр через 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: → [{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: → # [{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: → {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 } # Захват / (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() } } # Стандартные дополнения уровня таблицы (прямые дети ): извлечь ТОЛЬКО отклонения в карту # { тип: {свойства} }. Имя (=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 = направление (); behavior = (Авто = нет тега → ключ опускаем). # group = направление (). Нет тега → '' (тип-маркер сохраняется, направление # не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical' # — иначе компилятор додумает Vertical там, где его нет. $g = Get-Child $node 'Group' $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical' } 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 } } if ((Get-Child $node 'ShowTitle') -eq 'false') { $obj['showTitle'] = $false } $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 } $kids = Decompile-Children $node if ($kids) { $obj['children'] = $kids } } 'ColumnGroup' { # columnGroup = направление (). Нет тега → '' (тип-маркер сохраняется, направление # не эмитим). Иначе компилятор додумает Horizontal там, где его нет. $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 if ((Get-Child $node 'ShowTitle') -eq 'false') { $obj['showTitle'] = $false } $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 if ((Get-Child $node 'MultiLine') -eq 'true') { $obj['multiLine'] = $true } if ((Get-Child $node 'PasswordMode') -eq 'true') { $obj['passwordMode'] = $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 — типизированное (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 } } } $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' 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' $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } $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: тег (опечатка платформы), не 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') } Add-FormatProps $obj $node } 'PictureDecoration' { $obj[$key] = $name Add-CommonProps $obj $node $name # title декорации — единая ML-text форма с formatted (атрибут у PictureDecoration) $tiNode = $node.SelectSingleNode("lf:Title", $ns) if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } $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 } $sct = Get-Child $node 'Shortcut'; if ($sct) { $obj['shortcut'] = $sct } $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 } # 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: src (xr:Ref) + LoadTransparent (дефолт false). При true → объектная форма. $rpRef = $node.SelectSingleNode("lf:RowsPicture/xr:Ref", $ns) if ($rpRef) { $rpLt = $node.SelectSingleNode("lf:RowsPicture/xr:LoadTransparent", $ns) if ($rpLt -and $rpLt.InnerText -eq 'true') { $obj['rowsPicture'] = [ordered]@{ src = $rpRef.InnerText; loadTransparent = $true } } else { $obj['rowsPicture'] = $rpRef.InnerText } } $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' } if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] } if ((Get-Child $node 'ShowTitle') -eq 'false') { $obj['showTitle'] = $false } $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 } $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['picture'] = $ref.InnerText } # Дефолт у picture кнопки/попапа = true → фиксируем только отклонение false (true опускаем) $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'false') { $obj['loadTransparent'] = $false } $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 $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['picture'] = $ref.InnerText } # Дефолт у picture кнопки/попапа = true → фиксируем только отклонение false (true опускаем) $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'false') { $obj['loadTransparent'] = $false } $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) → объект { text?, … }. # Events компаньона пока НЕ захватываем (хвост — требует имя-обработчик). $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 } 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 } # --- 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') $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 } 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 } $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) } } # Форма скелета 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 } $tNode = $c.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } $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 } $ref = $c.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $co['picture'] = $ref.InnerText } # Дефолт у picture команды = true → фиксируем только отклонение false (true опускаем) $lt = $c.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'false') { $co['loadTransparent'] = $false } $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 }