# form-decompile v0.147 — 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 # Второй документ с сохранением whitespace — только для восстановления ТОЧНОГО числа пробелов # в whitespace-only (декорации-распорки без width: число пробелов = ширина). Основной # парс (PreserveWhitespace=false) не трогаем; элементная структура обоих документов идентична → # навигация по индекс-пути элементов (Resolve-WS). Загрузка лениво-безопасная. $script:xmlDocWS = $null try { $script:xmlDocWS = New-Object System.Xml.XmlDocument; $script:xmlDocWS.PreserveWhitespace = $true; $script:xmlDocWS.Load($FormPath) } catch { $script:xmlDocWS = $null } # 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, [bool]$hasGrouping = $false) 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) $code = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" # Контейнер может нести собственный (кастомная подпись # настройки) — сохраняем форму по xsi:type (Get-PresByType: ru-only LocalString ≠ xs:string). $uspNode = $child.SelectSingleNode("dcsset:userSettingPresentation", $ns) if ($uspNode) { $usp = Get-PresByType $uspNode $shape[$tag] = [ordered]@{ meta = $code; presentation = $usp } } else { $shape[$tag] = $code } } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } elseif ($tag -eq 'itemsUserSettingPresentation') { $shape['itemsUserSettingPresentation'] = Get-PresByType $child } # items-уровневая подпись (форма по xsi:type) elseif ($tag -eq 'dataParameters') { $shape['dataParameters'] = $true } # значения параметров запроса (контент в settings.dataParameters) elseif ($tag -eq 'item') { if ($hasGrouping) { $shape['structure'] = $true } else { return $null } } else { return $null } # неизвестный top-level → канон-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 } # Группировка строк динамического списка: цепочка (каждый уровень = # один groupItems-field; вложенность через дочерний ). Возвращает ПЛОСКИЙ массив уровней # (string для дефолтного поля; объект {field,groupType?,periodAdditionType?,periodAdditionBegin?,periodAdditionEnd?} # для нестандартного) ИЛИ $null, если структура не «чистая линейная цепочка одно-польных уровней» # (ветвление/мультиполе/доп.содержимое) → честный fallback на канон (LOST), а не тихая порча. function Build-GroupLevel { param($fn) $field = Get-Child $fn 'field' $gt = Get-Child $fn 'groupType' $pat = Get-Child $fn 'periodAdditionType' $pabN = $fn.SelectSingleNode("dcsset:periodAdditionBegin", $ns) $paeN = $fn.SelectSingleNode("dcsset:periodAdditionEnd", $ns) $pab = $null; $pae = $null if ($pabN) { $pt = $pabN.GetAttribute("type", $NS_XSI); $pv = $pabN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pab = $pv } } if ($paeN) { $pt = $paeN.GetAttribute("type", $NS_XSI); $pv = $paeN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pae = $pv } } $isDefault = ((-not $gt) -or $gt -eq 'Items') -and ((-not $pat) -or $pat -eq 'None') -and (-not $pab) -and (-not $pae) if ($isDefault) { return $field } $o = [ordered]@{ field = $field } if ($gt -and $gt -ne 'Items') { $o['groupType'] = $gt } if ($pat -and $pat -ne 'None') { $o['periodAdditionType'] = $pat } if ($pab) { $o['periodAdditionBegin'] = $pab } if ($pae) { $o['periodAdditionEnd'] = $pae } return $o } function Build-ListGrouping { param($itemNode) $levels = New-Object System.Collections.ArrayList $cur = $itemNode while ($cur) { if (($cur.GetAttribute("type", $NS_XSI)) -notmatch 'StructureItemGroup$') { return $null } $gi = $null; $nested = @() foreach ($ch in $cur.ChildNodes) { if ($ch.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } switch ($ch.LocalName) { 'groupItems' { if ($gi) { return $null }; $gi = $ch } 'item' { $nested += $ch } default { return $null } # use/name/filter/order/… — здесь не поддержано } } if (-not $gi) { return $null } $fieldItems = @($gi.SelectNodes("dcsset:item", $ns)) if ($fieldItems.Count -ne 1) { return $null } $fn = $fieldItems[0] if (($fn.GetAttribute("type", $NS_XSI)) -notmatch 'GroupItemField$') { return $null } [void]$levels.Add((Build-GroupLevel $fn)) if ($nested.Count -eq 0) { break } if ($nested.Count -gt 1) { return $null } # ветвление — не линейно $cur = $nested[0] } if ($levels.Count -eq 0) { return $null } return ,@($levels) } # --- 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$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') { Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings" } # Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с # префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые # дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны. elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match ']')) { Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings" } } # --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) --- function Convert-StringToJsonLiteral { param([string]$s) if ($null -eq $s) { return 'null' } $sb = New-Object System.Text.StringBuilder [void]$sb.Append('"') foreach ($ch in $s.ToCharArray()) { $code = [int]$ch if ($code -eq 0x22) { [void]$sb.Append('\"') } elseif ($code -eq 0x5C) { [void]$sb.Append('\\') } elseif ($code -eq 0x08) { [void]$sb.Append('\b') } elseif ($code -eq 0x09) { [void]$sb.Append('\t') } elseif ($code -eq 0x0A) { [void]$sb.Append('\n') } elseif ($code -eq 0x0C) { [void]$sb.Append('\f') } elseif ($code -eq 0x0D) { [void]$sb.Append('\r') } elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) } else { [void]$sb.Append($ch) } } [void]$sb.Append('"') return $sb.ToString() } function Try-InlineJson { param($obj) if ($null -eq $obj) { return 'null' } if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } if ($obj -is [int] -or $obj -is [long]) { return "$obj" } if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) } if ($obj -is [System.Collections.IDictionary]) { if ($obj.Count -eq 0) { return '{}' } $parts = @() foreach ($k in $obj.Keys) { $v = Try-InlineJson $obj[$k] if ($null -eq $v) { return $null } $parts += "$(Convert-StringToJsonLiteral "$k"): $v" } return '{ ' + ($parts -join ', ') + ' }' } if ($obj -is [array] -or $obj -is [System.Collections.IList]) { $items = @($obj) if ($items.Count -eq 0) { return '[]' } $parts = @() foreach ($it in $items) { $v = Try-InlineJson $it if ($null -eq $v) { return $null } $parts += $v } return '[' + ($parts -join ', ') + ']' } return $null } function ConvertTo-CompactJson { param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120) $indent = $indentUnit * $depth $childIndent = $indentUnit * ($depth + 1) if ($null -eq $obj) { return 'null' } if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } } if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) } if ($obj -is [int] -or $obj -is [long]) { return "$obj" } if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) { return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture)) } $isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList]) if ($isContainer) { $inlineAttempt = Try-InlineJson $obj if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt } } if ($obj -is [System.Collections.IDictionary]) { $keys = @($obj.Keys) if ($keys.Count -eq 0) { return '{}' } $parts = @() foreach ($k in $keys) { $val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit $parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val" } return "{`n" + ($parts -join ",`n") + "`n$indent}" } if ($obj -is [array] -or $obj -is [System.Collections.IList]) { $items = @($obj) if ($items.Count -eq 0) { return '[]' } $parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" }) return "[`n" + ($parts -join ",`n") + "`n$indent]" } return (Convert-StringToJsonLiteral "$obj") } # --- 2. Helpers --- # Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей. # Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы # (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions). $COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar') # Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...} function Get-LangText { param($node) if ($null -eq $node) { return $null } $items = @($node.SelectNodes("v8:item", $ns)) if ($items.Count -eq 0) { return $null } $map = [ordered]@{} foreach ($it in $items) { $lang = $it.SelectSingleNode("v8:lang", $ns) $content = $it.SelectSingleNode("v8:content", $ns) if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } } if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] } return $map } # Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает # → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой # Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue). # Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель # «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст. # Точное число пробелов whitespace-only из PreserveWhitespace-документа (основной # парс стрипает его в ""). Навигация по индекс-пути элементов (структура обоих документов идентична). function Resolve-WS { param($contentNode) if (-not $contentNode -or -not $script:xmlDocWS) { return $null } $idxs = New-Object System.Collections.ArrayList $cur = $contentNode while ($cur.ParentNode -and $cur.ParentNode.NodeType -eq [System.Xml.XmlNodeType]::Element) { $i = 0; $sib = $cur.PreviousSibling while ($sib) { if ($sib.NodeType -eq [System.Xml.XmlNodeType]::Element) { $i++ }; $sib = $sib.PreviousSibling } [void]$idxs.Insert(0, $i) $cur = $cur.ParentNode } $wcur = $script:xmlDocWS.DocumentElement foreach ($ix in $idxs) { $els = @(); foreach ($ch in $wcur.ChildNodes) { if ($ch.NodeType -eq [System.Xml.XmlNodeType]::Element) { $els += $ch } } if ($ix -ge $els.Count) { return $null } $wcur = $els[$ix] } return $wcur.InnerText } # Точное восстановление пробела (число): whitespace-only content → реальная строка пробелов из WS-дока. function Restore-WSContent { param($contentNode) $ws = Resolve-WS $contentNode if ($ws -and $ws.Trim() -eq '') { return $ws } # только если действительно whitespace return ' ' } function Get-LangTextWS { param($node) $t = Get-LangText $node if ($null -eq $t) { return $null } if ($t -is [string]) { $cn = $node.SelectSingleNode("v8:item/v8:content", $ns) if ($t -eq '' -and $cn) { return (Restore-WSContent $cn) } return $t } foreach ($it in @($node.SelectNodes("v8:item", $ns))) { $lang = $it.SelectSingleNode("v8:lang", $ns) $content = $it.SelectSingleNode("v8:content", $ns) if ($lang -and $content -and $t.Contains($lang.InnerText) -and $t[$lang.InnerText] -eq '') { $t[$lang.InnerText] = (Restore-WSContent $content) } } return $t } # Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile. # Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам. function Title-FromName { param([string]$name) if (-not $name) { return '' } $s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2') $s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2') $parts = $s -split ' ' if ($parts.Count -eq 0) { return $s } $out = New-Object System.Collections.ArrayList [void]$out.Add($parts[0]) for ($i = 1; $i -lt $parts.Count; $i++) { $p = $parts[$i] if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) } else { [void]$out.Add($p.ToLower()) } } return ($out -join ' ') } # Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!). $script:fmtMarkupRe = '|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)' function Test-HasRealMarkup { param($text) if ($null -eq $text) { return $false } $vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") } foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } } return $false } # Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted # совпал с атрибутом; иначе явный {text, formatted}. function Get-MLFormattedValue { param($titleNode) if (-not $titleNode) { return $null } # Get-LangTextWS восстанавливает значимый пробел в (одиночный и # мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "". $text = Get-LangTextWS $titleNode if ($null -eq $text) { return $null } $fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true') if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text } $o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o } # Прочитать дочерний скаляр (по local-name, без namespace) function Get-Child { param($node, [string]$name) $c = $node.SelectSingleNode("*[local-name()='$name']") if ($c) { return $c.InnerText } else { return $null } } function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") } function To-Bool { param([string]$v) return ($v -eq 'true') } # Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка). # Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type. function Convert-TypedValue { param([string]$raw, [string]$xsiType) switch -regex ($xsiType) { 'decimal$' { if ($raw -match '^-?\d+$') { return [int]$raw } return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture) } 'boolean$' { return ($raw -eq 'true') } default { return $raw } } } # ===================================================================== # Захват настроек компоновщика динамического списка (ListSettings): # filter / order / conditionalAppearance. Логика портирована из навыка # skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance # и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd # заменён на запись в stderr + пропуск элемента (form-decompile — draft, # скрипт не падает на непокрытых конструкциях). # ===================================================================== # Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text. function Get-Text { param($node, [string]$xpath) if (-not $node) { return $null } if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText } $n = $node.SelectSingleNode($xpath, $ns) if ($n) { return $n.InnerText } else { return $null } } # Мультиязычный текст (LocalStringType) → string (ru) или ordered hash. # Алиас на уже существующий Get-LangText (тот же контракт). function Get-MLText { param($node) return (Get-LangText $node) } # Презентация: либо мультиязычный LocalStringType, либо плоский xs:string. # Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText. function Get-PresText { param($node) if (-not $node) { return $null } $ml = Get-MLText $node if ($null -ne $ml) { return $ml } if ($node.InnerText) { return $node.InnerText } return $null } # Presentation, сохраняющий ФОРМУ по xsi:type (не схлопывает ru-only LocalStringType в строку): # <… xsi:type="v8:LocalStringType"> → объект {lang:text} (даже один ru), иначе xs:string → плоская строка. # Нужно, чтобы компилятор воспроизвёл точный xsi:type (LocalStringType vs xs:string). Пустой LocalStringType → $null. function Get-PresByType { param($node) if (-not $node) { return $null } $xt = $node.GetAttribute("type", $NS_XSI) if ($xt -match 'LocalStringType$') { $d = [ordered]@{} foreach ($it in $node.SelectNodes("v8:item", $ns)) { $lang = Get-Text $it "v8:lang"; $content = Get-Text $it "v8:content" if ($lang) { $d[$lang] = $content } } if ($d.Count -gt 0) { return $d } return $null } 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/Field/multilang/raw text. function Read-AppearanceValueNode { param($valNode) if (-not $valNode) { return $null } $vt = Get-LocalXsiType $valNode if ($vt -eq 'LocalStringType') { # НЕ схлопываем одноязычный в строку: значение параметра оформления различает # xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы # одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType. $map = [ordered]@{} foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) { $lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns) if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } } } return $map } if ($vt -eq 'Font') { return (Get-FontValue $valNode) } if ($vt -eq 'Line') { return (Get-LineValue $valNode) } # dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь} if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } } return $valNode.InnerText } # Обратная карта comparisonType → короткий оператор фильтра (зеркало skd). $script:filterOpMap = @{ 'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>='; 'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn'; 'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy'; 'Contains'='contains'; 'NotContains'='notContains'; 'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith'; 'Like'='like'; 'NotLike'='notLike'; 'Filled'='filled'; 'NotFilled'='notFilled' } # Render filter value node → shorthand-acceptable scalar string function Get-FilterValue { param($valNode) if (-not $valNode) { return '_' } $nil = $valNode.GetAttribute("nil", $NS_XSI) if ($nil -eq 'true') { return '_' } $vType = Get-LocalXsiType $valNode if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText } if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) } $txt = $valNode.InnerText if (-not $txt) { return '_' } return $txt } # Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field). function Get-FilterValueWithType { param($valNode) if (-not $valNode) { return @{ value = '_'; type = $null } } $rawType = $valNode.GetAttribute("type", $NS_XSI) $nil = $valNode.GetAttribute("nil", $NS_XSI) if ($nil -eq 'true') { return @{ value = '_'; type = $null } } $vType = Get-LocalXsiType $valNode if ($vType -eq 'LocalStringType') { return @{ value = (Get-MLText $valNode); type = $rawType } } # Стандартная дата начала/окончания. Формы (от самой компактной): # SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт # даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType; # SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал. if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') { $variantN = $valNode.SelectSingleNode("v8:variant", $ns) $dateN = $valNode.SelectSingleNode("v8:date", $ns) $variantStr = if ($variantN) { $variantN.InnerText } else { '' } if ($dateN) { if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') { return @{ value = $dateN.InnerText; type = $null } # голая дата = SBD Custom шорткат } return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType } } return @{ value = $variantStr; type = $rawType } # именованный вариант } $txt = $valNode.InnerText if (-not $txt) { return @{ value = '_'; type = $rawType } } if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } } if ($vType -eq 'decimal') { if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } } return @{ value = [double]$txt; type = $rawType } } return @{ value = $txt; type = $rawType } } # Convert filter item node → shorthand string или object form (рекурсивно для групп). function Build-FilterItem { param($itemNode, [string]$loc) $xtype = Get-LocalXsiType $itemNode if ($xtype -eq 'FilterItemGroup') { $gt = Get-Text $itemNode "dcsset:groupType" $groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } } $items = @() foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) { $bi = (Build-FilterItem -itemNode $c -loc "$loc/item") if ($null -ne $bi) { $items += $bi } } $gObj = [ordered]@{ group = $groupName; items = $items } if ((Get-Text $itemNode "dcsset:use") -eq 'false') { $gObj['use'] = $false } # группа отключена (@off) $gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns) if ($gPresNode) { # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) $gPres = Get-PresByType $gPresNode if ($null -ne $gPres -and $gPres -ne '') { $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 ($vt.type -eq 'xs:string' -and ($value -is [string]) -and ($value -match '^(-?\d+(\.\d+)?|\d{4}-\d{2}-\d{2}T)')) { # Значение-строка "1"/"2020-..." с xsi:type="xs:string": компилятор авто-детектит число/дату # (теряет xs:string). Когда авто-вывод дал бы ДРУГОЙ тип — фиксируем явный valueType. $valueTypeAttr = 'xs:string' } } } 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) { # Сохраняем форму по xsi:type (LocalStringType ru-only ≠ xs:string) $fiPres = Get-PresByType $fiPresNode } $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') # Пустой xs:string right () ≠ отсутствие : # Get-FilterValueWithType маппит наличие пустого/nil right в value='_' (отсутствие → $null). # Shorthand `_` неоднозначен (схлопывает оба, компилятор → нет right). Пробельные значения рвут # shorthand-парсинг (split по пробелам). → форсим объектную форму (value='_' → self-closing; # пробелы — как есть в ). $valNeedsObj = $false if ($rightNodes.Count -eq 1 -and -not $valueIsArrayFlag -and $op -notin $noValueOps) { if ("$value" -eq '_') { # Truly-empty ИЛИ whitespace-only # (PreserveWhitespace=false стрипнул пробелы в '' → Get-FilterValueWithType вернул '_'). # Восстанавливаем реальные пробелы из WS-дока (как у whitespace-заголовков). $ws = Resolve-WS $rightNodes[0] if ($ws -and $ws.Length -gt 0 -and $ws.Trim() -eq '') { $value = $ws } $valNeedsObj = $true } elseif ($null -ne $value -and "$value" -match '\s') { $valNeedsObj = $true } } if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres -or $valNeedsObj) { $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) { # Сохраняем форму по xsi:type: LocalStringType (даже ru-only) → объект → компилятор эмитит LocalStringType. $pres = Get-PresByType $presNode if ($null -ne $pres -and $pres -ne '') { $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 } # ColumnGroup: динамический заголовок из данных + формат заголовка (ML-текст) $hdp = Get-Child $node 'HeaderDataPath'; if ($hdp) { $obj['headerDataPath'] = $hdp } $hfNode = $node.SelectSingleNode("lf:HeaderFormat", $ns); if ($hfNode) { $hf = Get-LangText $hfNode; if ($null -ne $hf) { $obj['headerFormat'] = $hf } } } # 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-LangTextWS $ctNode; if ($null -ne $t) { $co['title'] = $t } } $cfc = Get-Child $c 'FillCheck'; if ($cfc) { $co['fillCheck'] = $cfc } # проверка заполнения колонки (как у реквизита) $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } # Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита (bool | {common,roles}) $cv = Decompile-XrFlag $c 'View'; if ($null -ne $cv) { $co['view'] = $cv } $ce = Decompile-XrFlag $c 'Edit'; if ($null -ne $ce) { $co['edit'] = $ce } return $co } # Общие свойства элемента (visible/enabled/readonly/title/events) → в hash # Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture). # Платформа ВСЕГДА эмитит (и true, и false) → дефолт DSL = false. # Источник: (именованная/стилевая) ИЛИ (встроенная → префикс "abs:"). # Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект # {src, loadTransparent?, transparentPixel?}. function Get-PictureRef { param($node, [string]$picTag) $ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns) $abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns) if (-not $ref -and -not $abs) { return $null } $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } $lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns) $ltTrue = ($lt -and $lt.InnerText -eq 'true') $tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns) if (-not $ltTrue -and -not $tpx) { return $src } $o = [ordered]@{ src = $src } if ($ltTrue) { $o['loadTransparent'] = $true } if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } return $o } # кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values): # фиксируем только отклонение false. Источник или (→ "abs:"). При наличии # → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture # + отдельный loadTransparent:false. function Set-CommandPicture { param($obj, $node) $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) if (-not $ref -and -not $abs) { return } $src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" } $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns) $ltFalse = ($lt -and $lt.InnerText -eq 'false') $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) if ($tpx) { $o = [ordered]@{ src = $src } if ($ltFalse) { $o['loadTransparent'] = $false } $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } $obj['picture'] = $o } else { $obj['picture'] = $src if ($ltFalse) { $obj['loadTransparent'] = $false } } } # Шрифт → строка-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-LangTextWS $titleNode # восстановление значимого пробела (whitespace-заголовок) if ($null -ne $t) { $obj['title'] = $t } # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) } $ttNode = $node.SelectSingleNode("lf:ToolTip", $ns) if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } } $ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr } # Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon) $hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp } $fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp } $ev = Get-Events $node $elName if ($ev) { $obj['events'] = $ev } # CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture): # список отключённых команд редактора. Только , пустого не бывает. $csNode = $node.SelectSingleNode("lf:CommandSet", $ns) if ($csNode) { $exc = New-Object System.Collections.ArrayList foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) } if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) } } } # --- 3. Type decompile (inverse of Emit-Type) --- function Decompile-Type { param($typeNode) if (-not $typeNode) { return $null } $parts = New-Object System.Collections.ArrayList foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) { $raw = $vt.InnerText.Trim() $short = $raw # break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList). switch -regex ($raw) { '^xs:string$' { $len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) $al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns) $fixed = ($al -and $al.InnerText -eq 'Fixed') # Variable = дефолт (опускаем); Fixed — явно if ($len -and [int]$len.InnerText -gt 0) { $short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" } } else { $short = "string" } # Length=0 → всегда Variable (корпус) break } '^xs:decimal$' { $d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns) $f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns) $sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns) $dd = if ($d) { $d.InnerText } else { '0' } $ff = if ($f) { $f.InnerText } else { '0' } if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" } break } '^xs:boolean$' { $short = "boolean"; break } '^xs:dateTime$' { $df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) $dfv = if ($df) { $df.InnerText } else { 'DateTime' } switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } } break } '^cfg:(.+)$' { $short = $matches[1]; break } '^(v8|v8ui):' { # Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом # (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост. $rev = @{ 'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList' 'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal' 'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure' 'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font' } if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw } break } default { $short = $raw } } [void]$parts.Add($short) } # TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида». # Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена. foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) { $raw = $ts.InnerText.Trim() $short = $raw -replace '^(v8ui|v8|cfg):', '' [void]$parts.Add($short) } # TypeId — тип, заданный глобальным стабильным GUID (, не ). Платформа так # сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально # стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID). foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) { [void]$parts.Add("typeid:" + $ti.InnerText.Trim()) } if ($parts.Count -eq 0) { return $null } if ($parts.Count -eq 1) { return $parts[0] } return ($parts -join ' | ') } # Ограничения использования (useRestriction/attributeUseRestriction) → объект {field?,condition?,group?,order?}. function Build-RestrictObj { param($node) $r = [ordered]@{} foreach ($k in 'field','condition','group','order') { if ((Get-Child $node $k) -eq 'true') { $r[$k] = $true } } return $r } # Вычисляемое поле DataSet динамического списка () → объектная модель. # Зеркало эмиссии form-compile (Emit-CalcFields). Грамматика — как skd calculatedFields, # + форм-extras presentationExpression/orderExpression (в namespace dcscommon). function Build-CalcField { param($cfNode) $o = [ordered]@{} $o['dataPath'] = Get-Child $cfNode 'dataPath' $o['expression'] = Get-Child $cfNode 'expression' $tn = $cfNode.SelectSingleNode("dcssch:title", $ns) if ($tn) { $t = Get-LangText $tn; if ($null -ne $t) { $o['title'] = $t } } $vt = $cfNode.SelectSingleNode("dcssch:valueType", $ns) if ($vt) { $v = Decompile-Type $vt; if ($v) { $o['valueType'] = $v } } $ur = $cfNode.SelectSingleNode("dcssch:useRestriction", $ns) if ($ur) { $r = [ordered]@{} foreach ($k in 'field','condition','group','order') { if ((Get-Child $ur $k) -eq 'true') { $r[$k] = $true } } if ($r.Count -gt 0) { $o['useRestriction'] = $r } } $pe = Get-Child $cfNode 'presentationExpression' if ($null -ne $pe -and $pe -ne '') { $o['presentationExpression'] = $pe } $oeNodes = @($cfNode.SelectNodes("dcssch:orderExpression", $ns)) if ($oeNodes.Count -gt 0) { $oes = New-Object System.Collections.ArrayList foreach ($oen in $oeNodes) { $eo = [ordered]@{} $exprN = $oen.SelectSingleNode("*[local-name()='expression']") $otN = $oen.SelectSingleNode("*[local-name()='orderType']") $aoN = $oen.SelectSingleNode("*[local-name()='autoOrder']") $eo['expression'] = if ($exprN) { $exprN.InnerText } else { '' } if ($otN -and $otN.InnerText -ne 'Asc') { $eo['orderType'] = $otN.InnerText } if ($aoN -and $aoN.InnerText -eq 'true') { $eo['autoOrder'] = $true } [void]$oes.Add($eo) } $o['orderExpression'] = @($oes) } return $o } # 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) } elseif ($vt -match 'TypeLink$') { # Связь по типу (dcscor:TypeLink): field + linkItem — структурное значение, # НЕ склеивать InnerText в строку ("СчетДт"+"1"="СчетДт1"). $tlo = [ordered]@{} $tlf = Get-Child $valN 'field'; if ($null -ne $tlf) { $tlo['field'] = $tlf } $tli = Get-Child $valN 'linkItem'; if ($null -ne $tli) { $tlo['linkItem'] = if ($tli -match '^-?\d+$') { [int]$tli } else { $tli } } $io['typeLink'] = $tlo } else { if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt } } } [void]$items.Add($io) } return @($items) } # dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного # значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально, # в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level). function Build-FormDataParameters { param($dpNode) $entries = @() foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) { $pn = Get-Text $it "dcscor:parameter" $use = Get-Text $it "dcscor:use" $valNodes = @($it.SelectNodes("dcscor:value", $ns)) $valNode = if ($valNodes.Count -ge 1) { $valNodes[0] } else { $null } $usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns) $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) if ($valNode -or $usidN -or $vmN -or $uspN) { $obj = [ordered]@{ parameter = $pn } if ($valNodes.Count -gt 1) { # Список значений параметра (valueListAllowed) — захватываем ВСЕ массивом $obj['value'] = @($valNodes | ForEach-Object { $_.InnerText }) $vt0 = $valNodes[0].GetAttribute("type", $NS_XSI); if ($vt0) { $obj['valueType'] = $vt0 } } elseif ($valNode) { if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true } else { $vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal } elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') } else { $obj['value'] = $vVal } if ($vType) { $obj['valueType'] = $vType } } } if ($use -eq 'false') { $obj['use'] = $false } if ($usidN) { $obj['userSettingID'] = 'auto' } if ($vmN) { $obj['viewMode'] = $vmN.InnerText } if ($uspN) { $usp = Get-PresText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } } $entries += $obj } else { $s = $pn; if ($use -eq 'false') { $s += ' @off' } $entries += $s } } return ,$entries } function Build-DLParameter { param($pNode) $name = Get-Child $pNode 'name' $o = [ordered]@{} $o['name'] = $name # title — опускаем, если совпадает с авто-выводом из имени (ru-only) $titleNode = $pNode.SelectSingleNode("dcssch:title", $ns) if ($titleNode) { $t = Get-LangText $titleNode if ($null -ne $t) { $auto = Title-FromName -name $name if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t } } } # valueType $vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns) $typeVal = $null if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } } # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет # не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит → # явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.) $vNodes = @($pNode.SelectNodes("dcssch:value", $ns)) if ($vNodes.Count -gt 1) { # valueListAllowed: список значений — захватываем ВСЕ массивом $o['value'] = @($vNodes | ForEach-Object { Convert-TypedValue -raw $_.InnerText -xsiType ($_.GetAttribute("type", $NS_XSI)) }) } elseif ($vNodes.Count -eq 1) { $vNode = $vNodes[0] if ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true') { $o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI)) } elseif ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['value'] = $null } } # useRestriction — опускаем true (дефолт), фиксируем false if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false } # expression $expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr } # availableValues $avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns)) if ($avNodes.Count -gt 0) { $avs = New-Object System.Collections.ArrayList foreach ($avn in $avNodes) { $avo = [ordered]@{} $avv = $avn.SelectSingleNode("dcssch:value", $ns) if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) } else { $avo['value'] = $null } $avp = $avn.SelectSingleNode("dcssch:presentation", $ns) if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } } [void]$avs.Add($avo) } $o['availableValues'] = @($avs) } # valueListAllowed / availableAsField if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true } if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false } # inputParameters $ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns) if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } } # denyIncompleteValues / use if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true } $use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use } # Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект. $keys = @($o.Keys) if ($keys.Count -eq 1) { return $name } if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) } return $o } # --- 4. Element dispatch --- $ELEMENT_KEY = @{ 'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check'; 'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField'; 'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar'; 'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup'; 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl'; 'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc'; 'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar'; 'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner'; 'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart' } # Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value. $GENERIC_SCALARS = @( @{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' } @{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' } @{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' } @{ Tag='PictureSize'; Key='pictureSize'; Kind='value' } @{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' } @{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' } @{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' } @{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' } @{ Tag='ViewMode'; Key='viewMode'; Kind='value' } @{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' } @{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' } @{ Tag='Mask'; Key='mask'; Kind='value' } @{ Tag='CreateButton'; Key='createButton'; Kind='bool' } @{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' } @{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' } # Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора) @{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' } @{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' } @{ Tag='Output'; Key='output'; Kind='value' } @{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' } @{ Tag='PointerType'; Key='pointerType'; Kind='value' } @{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' } @{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' } @{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' } @{ Tag='Protection'; Key='protection'; Kind='bool' } @{ Tag='Edit'; Key='edit'; Kind='bool' } @{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' } @{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' } @{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' } @{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' } @{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' } @{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' } # Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы @{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' } @{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' } @{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' } # Хвост: высота элемента списка (radio) / ширина выпадающего списка (input) @{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' } @{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' } # Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам @{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' } @{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' } @{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' } @{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' } @{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' } @{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' } # Корпусный хвост (zеркало компилятора): свёртка группы / форма попапа / авто-добавление / # выделение отрицательных / нач. позиция списка / высота списка выбора / три состояния / прокрутка @{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' } @{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' } @{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' } @{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' } @{ Tag='InitialListView'; Key='initialListView'; Kind='value' } @{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' } @{ Tag='ThreeState'; Key='threeState'; Kind='bool' } @{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' } # Сочетание клавиш — общее свойство элемента (команда — отдельный путь) @{ Tag='Shortcut'; Key='shortcut'; Kind='value' } # Батч простых скаляров (зеркало компилятора; Table HeaderHeight/FooterHeight/CurrentRowUse — отдельно) @{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' } @{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' } @{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' } @{ Tag='ImageScale'; Key='imageScale'; Kind='value' } @{ Tag='Zoomable'; Key='zoomable'; Kind='bool' } @{ Tag='Shape'; Key='shape'; Kind='value' } @{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' } # Равная ширина элементов (check/radio) / высота заголовка пункта (radio) @{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' } @{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' } # Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр @{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' } # Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input) @{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' } @{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' } @{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' } @{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' } # Режим авто-показа кнопок открытия/очистки (input, enum) @{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' } @{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' } # Оформление/картинка множественного выбора (input, редко; цвета — текст-контент) @{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' } @{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' } @{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' } @{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' } # Хвост листовых скаляров (по 1): автокоррекция / уникальность команды / пустое множ.значение / гориз.сжатие @{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' } @{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' } @{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' } @{ Tag='AllowInputEmptyMultipleValues'; Key='allowInputEmptyMultipleValues'; Kind='bool' } @{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; 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) { if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { # nil-значение элемента choiceList — компилятор эмитит # (иначе Convert-TypedValue вернул бы "" → typed-empty xs:string). $ci['valueType'] = 'nil' } else { $xsiType = $valNode.GetAttribute("type", $NS_XSI) $ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType # Системное перечисление (ent:*) / иной не-примитивный тип → сохраняем valueType # (Normalize-ChoiceValue вывела бы xs:string). DesignTimeRef обычно авто-детектится # компилятором по named-ref (Enum.X.Y), НО raw-ссылка по GUID (GUID.GUID) — нет → сохраняем. if ($xsiType -and $xsiType -notmatch '^xs:(string|decimal|boolean|dateTime)$' -and ` ($xsiType -ne 'xr:DesignTimeRef' -or "$($valNode.InnerText)" -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-')) { $ci['valueType'] = $xsiType } } } # Presentation: непустой → текст/мультиязык; пустой → "" — суппресс-маркер, # подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения). if ($presNode) { $p = Get-LangTextWS $presNode # восстановление значимого пробела (whitespace-presentation) 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'; 'HorizontalIfPossible'='horizontalIfPossible' } if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' } $behavior = Get-Child $node 'Behavior' if ($behavior) { $bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' } if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior } } $obj['name'] = $name Add-CommonProps $obj $node $name $rep = Get-Child $node 'Representation' if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } } $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } # использование текущей строки группы $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 } # Формат значения пути к данным заголовка (; парный к titleDataPath группы) Add-FormatProps $obj $node $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 $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) $sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) } $kids = Decompile-Children $node if ($kids) { $obj['children'] = $kids } } 'InputField' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name # MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true») $mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') } # PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true») $pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') } $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } $ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangTextWS $ih; if ($t) { $obj['inputHint'] = $t } } $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $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 } } } # Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор. # availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type. $tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) } $atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } } $cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr } $cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp } if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false } $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } Add-FormatProps $obj $node # Параметры выбора / Связи параметров выбора / Связь по типу $cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp } $cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl } $tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk } } 'CheckBoxField' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } # CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение $cbt = Get-Child $node 'CheckBoxType' if ($null -eq $cbt) { $obj['checkBoxType'] = '' } elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) } Add-TitleLocation $obj $node 'Right' $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } Add-FormatProps $obj $node } 'RadioButtonField' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name Add-TitleLocation $obj $node 'None' $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } $rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt } $cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc } $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } } 'LabelDecoration' { $obj[$key] = $name Add-CommonProps $obj $node $name if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } # title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip) $tiNode = $node.SelectSingleNode("lf:Title", $ns) if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } } 'LabelField' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } # LabelField: тег (опечатка платформы), не if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true } # PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения $pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') } $woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangTextWS $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } } # FooterDataPath / FooterText — общие cell-свойства колонки (как у input), не только input $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } Add-FormatProps $obj $node } 'PictureDecoration' { $obj[$key] = $name Add-CommonProps $obj $node $name # title декорации — единая ML-text форма с formatted (атрибут у PictureDecoration) $tiNode = $node.SelectSingleNode("lf:Title", $ns) if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } } $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } $ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns) $abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns) if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" } # встроенная картинка → префикс abs: $lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true } # Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности $tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns) if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } } if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } } 'PictureField' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name $em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em } $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true } $vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp } $npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangTextWS $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } } # FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField) $fdp = Get-Child $node 'FooterDataPath'; if ($fdp) { $obj['footerDataPath'] = $fdp } $ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangTextWS $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } } } 'CalendarField' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } $sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm } $scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') } $wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim } $him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him } $smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') } } 'Table' { $obj[$key] = $name $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } Add-CommonProps $obj $node $name $tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() } $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } $crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') } $cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') } if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true } # enableDrag — теперь общий (Add-Layout, фактическое значение) if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null } if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false } if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true } # Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout) $htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr } # Высота шапки/подвала таблицы в строках (pass-through; компилятор эмитит в Emit-Table) $hh = Get-Child $node 'HeaderHeight'; if ($null -ne $hh) { $obj['headerHeight'] = [int]$hh } $fh = Get-Child $node 'FooterHeight'; if ($null -ne $fh) { $obj['footerHeight'] = [int]$fh } # Использование текущей строки (Table-уровень; ≠ command-level CurrentRowUse) — pass-through $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } # Запрос обновления дин-списка (всегда PullFromTop в корпусе) — pass-through $rr = Get-Child $node 'RefreshRequest'; if ($rr) { $obj['refreshRequest'] = $rr } # CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем # (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват). $cbl = Get-Child $node 'CommandBarLocation' if (Has-Child $node 'UpdateOnDataChange') { if ($null -eq $cbl) { $obj['commandBarLocation'] = '' } elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl } } elseif ($cbl) { $obj['commandBarLocation'] = $cbl } $ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl } $vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl } $scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl } # --- Общие свойства таблицы (любой тип таблицы, не только динсписок) --- if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true } $selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm } $rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm } if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false } if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false } if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true } # Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill). $taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') } if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true } $soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin } $mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') } $itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv } # RowsPicture — конвенция ValuesPicture (Ref/Abs + LoadTransparent дефолт false + TransparentPixel) $rp = Get-PictureRef $node 'RowsPicture'; if ($null -ne $rp) { $obj['rowsPicture'] = $rp } $rpdp = Get-Child $node 'RowPictureDataPath' # --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) --- if (Has-Child $node 'UpdateOnDataChange') { if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true } $arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp } $cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi } if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true } if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false } if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true } $uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc } if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false } # RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' } elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp } $usg = Get-Child $node 'UserSettingsGroup' if ($usg) { if ($usg -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: UserSettingsGroup '$usg' ($name) — ссылка по id, не воспроизводима, опущена") } else { $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 } $cru = Get-Child $node 'CurrentRowUse'; if ($cru) { $obj['currentRowUse'] = $cru } $kids = Decompile-Children $node if ($kids) { $obj['children'] = $kids } } 'Page' { $obj[$key] = $name Add-CommonProps $obj $node $name $g = Get-Child $node 'Group' $gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' } if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] } # Картинка страницы (иконка вкладки) — конвенция ValuesPicture (дефолт LoadTransparent=false) $pp = Get-PictureRef $node 'Picture'; if ($null -ne $pp) { $obj['picture'] = $pp } $st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже) # Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы) Add-FormatProps $obj $node $kids = Decompile-Children $node if ($kids) { $obj['children'] = $kids } } 'Button' { $obj[$key] = $name $cmd = Get-Child $node 'CommandName' if ($cmd) { if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] } elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] } elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" } else { $obj['commandName'] = $cmd } } $dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp } # Parameter команды: xr:MDObjectRef (объект метаданных, строка) или v8:TypeDescription (тип → {type}) $btnParam = $node.SelectSingleNode("lf:Parameter", $ns) if ($btnParam) { $pxt = $btnParam.GetAttribute("type", $NS_XSI) if ($pxt -match 'TypeDescription$') { $pt = Decompile-Type $btnParam; if ($pt) { $obj['parameter'] = [ordered]@{ type = $pt } } } elseif ($btnParam.InnerText) { $obj['parameter'] = $btnParam.InnerText } } Add-CommonProps $obj $node $name $type = Get-Child $node 'Type' if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } } if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true } if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true } Set-CommandPicture $obj $node $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } $lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic } } 'ButtonGroup' { $obj[$key] = $name Add-CommonProps $obj $node $name $cs = Get-Child $node 'CommandSource' if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $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) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } $hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() } if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true } $kids = Decompile-Children $node if ($kids) { $obj['children'] = $kids } } 'Popup' { $obj[$key] = $name Add-CommonProps $obj $node $name Set-CommandPicture $obj $node $rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep } # Источник команд попапа (Form / FormCommandPanelGlobalCommands / Item.X) — как у ButtonGroup/CommandBar $cs = Get-Child $node 'CommandSource' if ($cs) { if ($cs -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: CommandSource '$cs' ($name) — ссылка по id, не воспроизводима, опущена") } else { $obj['commandSource'] = $cs } } $kids = Decompile-Children $node if ($kids) { $obj['children'] = $kids } } 'SearchStringAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } 'ViewStatusAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } 'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name } 'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key } 'HTMLDocumentField' { Decompile-SimpleField $obj $node $name $key } 'TextDocumentField' { Decompile-SimpleField $obj $node $name $key } 'FormattedDocumentField' { Decompile-SimpleField $obj $node $name $key } 'ProgressBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') } 'TrackBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') } 'ChartField' { Decompile-SimpleField $obj $node $name $key } 'GraphicalSchemaField' { Decompile-SimpleField $obj $node $name $key } 'PlannerField' { Decompile-SimpleField $obj $node $name $key } 'PeriodField' { Decompile-SimpleField $obj $node $name $key } 'DendrogramField' { Decompile-SimpleField $obj $node $name $key } 'GanttChartField' { Decompile-SimpleField $obj $node $name $key # Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element. # Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере). $tblNode = $node.SelectSingleNode("lf:Table", $ns) if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode } } } # DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть». $di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di } # title: "" — подавление авто-вывода: для типов, где компилятор вывел бы # заголовок из имени, а в оригинале <Title> отсутствует. if (-not $obj.Contains('title')) { $autoTitle = $false if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true } elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) } elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') } if ($autoTitle) { $obj['title'] = '' } } Add-Layout $obj $node Add-GenericScalars $obj $node # extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) → # строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink/events) → объект { text?, … }. $etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns) if ($etNode) { $etTitle = $etNode.SelectSingleNode("lf:Title", $ns) $textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null } $etObj = [ordered]@{} Add-Layout $etObj $etNode Add-GenericScalars $etObj $etNode Add-Appearance $etObj $etNode # ToolTip самого компаньона (подсказка расширенной подсказки) — реальный текст (ML), не пустой Title $etTT = $etNode.SelectSingleNode("lf:ToolTip", $ns) if ($etTT) { $ttVal = Get-LangText $etTT; if ($null -ne $ttVal) { $etObj['tooltip'] = $ttVal } } if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true } if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true } if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true } # События компаньона (напр. URLProcessing у hyperlink-подсказки) — переиспользуем механизм событий элемента $etEv = Get-Events $etNode $name; if ($etEv) { $etObj['events'] = $etEv } if ($etObj.Count -gt 0) { if ($null -ne $textVal) { if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } } else { $etObj['text'] = $textVal } # formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true } } $obj['extendedTooltip'] = $etObj } elseif ($null -ne $textVal) { $obj['extendedTooltip'] = $textVal } } # companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент) $isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange') $cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable if ($null -ne $cb) { $obj['commandBar'] = $cb } $cm = Decompile-CompanionPanel $node 'ContextMenu' if ($null -ne $cm) { $obj['contextMenu'] = $cm } return $obj } # ───────────────────────────────────────────────────────────────────────────── # Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите. # Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings. function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') } function PLD-Int { param($v) if ($null -eq $v) { return $null } return [int]$v } # <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null. function Get-PlannerValue { param($node) if (-not $node) { return $null } if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null } if ($node.InnerText) { return $node.InnerText } else { return $null } } function Build-PlannerFont { param($node) if (-not $node) { return $null } $o = [ordered]@{} foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) { $av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av } } if ($o.Count -eq 0) { return $null } return $o } function Build-PlannerBorder { param($node) if (-not $node) { return $null } $o = [ordered]@{} $w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } $st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText } return $o } function Build-PlannerItem { param($itn) $o = [ordered]@{} $valNode = $itn.SelectSingleNode("*[local-name()='value']") if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText } $o['text'] = (Get-Child $itn 'text') $tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt } $o['begin'] = (Get-Child $itn 'begin') $o['end'] = (Get-Child $itn 'end') $o['borderColor'] = (Get-Child $itn 'borderColor') $o['backColor'] = (Get-Child $itn 'backColor') $o['textColor'] = (Get-Child $itn 'textColor') $fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } $o['replacementDate'] = (Get-Child $itn 'replacementDate') $o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted')) $o['id'] = (Get-Child $itn 'id') $o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted')) $brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd } $o['editMode'] = (Get-Child $itn 'editMode') return $o } function Build-PlannerDimElement { param($eln) $o = [ordered]@{} $v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } $o['text'] = (Get-Child $eln 'text') $o['borderColor'] = (Get-Child $eln 'borderColor') $o['backColor'] = (Get-Child $eln 'backColor') $o['textColor'] = (Get-Child $eln 'textColor') $fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } $subs = New-Object System.Collections.ArrayList foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) } if ($subs.Count -gt 0) { $o['elements'] = @($subs) } $sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') } $o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted')) return $o } function Build-PlannerDimension { param($dn) $o = [ordered]@{} $v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v } $o['text'] = (Get-Child $dn 'text') $o['borderColor'] = (Get-Child $dn 'borderColor') $o['backColor'] = (Get-Child $dn 'backColor') $o['textColor'] = (Get-Child $dn 'textColor') $fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt } $els = New-Object System.Collections.ArrayList foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) } if ($els.Count -gt 0) { $o['elements'] = @($els) } $o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted')) return $o } function Build-PlannerLevel { param($lvn) $o = [ordered]@{} $o['measure'] = (Get-Child $lvn 'measure') $o['interval'] = (PLD-Int (Get-Child $lvn 'interval')) $o['show'] = (PLD-Bool (Get-Child $lvn 'show')) $lineNode = $lvn.SelectSingleNode("*[local-name()='line']") if ($lineNode) { $ln = [ordered]@{} $w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w } $g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') } $st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText } $o['line'] = $ln } $o['scaleColor'] = (Get-Child $lvn 'scaleColor') $o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule') $fmtNode = $lvn.SelectSingleNode("*[local-name()='format']") if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } } $labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']") if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } } $o['backColor'] = (Get-Child $lvn 'backColor') $o['textColor'] = (Get-Child $lvn 'textColor') $o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels')) return $o } function Build-PlannerTimeScale { param($tsn) $o = [ordered]@{} $o['placement'] = (Get-Child $tsn 'placement') $levels = New-Object System.Collections.ArrayList foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) } $o['levels'] = @($levels) $o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent')) $o['backColor'] = (Get-Child $tsn 'backColor') $o['textColor'] = (Get-Child $tsn 'textColor') $o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel')) return $o } function Build-PlannerSettings { param($setNode) $pl = [ordered]@{} $itemNodes = @($setNode.SelectNodes("*[local-name()='item']")) if ($itemNodes.Count -gt 0) { $items = New-Object System.Collections.ArrayList foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) } $pl['items'] = @($items) } $dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']")) if ($dimNodes.Count -gt 0) { $dims = New-Object System.Collections.ArrayList foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) } $pl['dimensions'] = @($dims) } $pl['borderColor'] = (Get-Child $setNode 'borderColor') $pl['backColor'] = (Get-Child $setNode 'backColor') $pl['textColor'] = (Get-Child $setNode 'textColor') $pl['lineColor'] = (Get-Child $setNode 'lineColor') $fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt } $pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod') $pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod') $pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale')) $pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders')) $pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders')) $wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']") if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } } $pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit') $pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition')) $pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent')) $pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent')) $tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']") if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) } $perNode = $setNode.SelectSingleNode("*[local-name()='period']") if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } } $pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate')) $pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation') $pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient') $pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth')) $pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight')) $pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth')) $pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight')) $pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader') $pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader') $brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd } $pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType') return $pl } # ───────────────────────────────────────────────────────────────────────────── # Chart design-time <Settings xsi:type="d4p1:Chart"> → объект chart. Генерик-движок: # рекурсивный захват поддерева d4p1; структуры (line/border/font/ML/области/серии) # детектируются по форме узла. Малые name-set'ы: ML-поля (даже ru-only/пустые → ML), # серии (всегда массив), attrs-узлы. Порядок ключей JSON = порядок XML (раундтрип). $CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 } $CHART_SERIES_FIELDS = @{ 'realSeriesData'=1;'realExSeriesData'=1;'realPointData'=1;'realDataItems'=1 } $CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 } function Conv-ChartScalar { param([string]$v) if ($v -eq 'true') { return $true } if ($v -eq 'false') { return $false } return $v } function Build-ChartNode { param($n, [string]$name) # ML-поле → строка/мапа/"" (даже ru-only форсим в ML на эмите по имени) if ($CHART_ML_FIELDS.Contains($name)) { $ml = Get-LangText $n if ($null -eq $ml) { return '' } else { return $ml } } $kids = @($n.SelectNodes("*")) if ($kids.Count -eq 0) { # лист: attrs-only (шрифт/gaugeQualityBands) или текст $attrs = @($n.Attributes | Where-Object { $_.Name -ne 'xmlns' -and -not $_.Name.StartsWith('xmlns:') -and $_.Name -ne 'xsi:type' -and $_.Name -ne 'xsi:nil' }) if ($attrs.Count -gt 0) { $o = [ordered]@{}; foreach ($a in $attrs) { $o[$a.Name] = (Conv-ChartScalar $a.Value) }; return $o } return (Conv-ChartScalar $n.InnerText) } # line/border: дочерний v8ui:style (+ width[/gap]) $styleChild = $n.SelectSingleNode("*[local-name()='style']") if ($styleChild) { $o = [ordered]@{} $w = $n.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w } $g = $n.GetAttribute('gap'); if ($g -ne '') { $o['gap'] = ($g -eq 'true') } $o['style'] = $styleChild.InnerText return $o } # вложенный объект d4p1 (область/шкала/titleArea/серия): группируем детей по имени $o = [ordered]@{} foreach ($c in $kids) { $ln = $c.LocalName $val = Build-ChartNode $c $ln if ($CHART_SERIES_FIELDS.Contains($ln)) { if (-not $o.Contains($ln)) { $o[$ln] = New-Object System.Collections.ArrayList } [void]$o[$ln].Add($val) } elseif ($o.Contains($ln)) { if ($o[$ln] -isnot [System.Collections.IList]) { $tmp = New-Object System.Collections.ArrayList; [void]$tmp.Add($o[$ln]); $o[$ln] = $tmp } [void]$o[$ln].Add($val) } else { $o[$ln] = $val } } # нормализуем ArrayList → @() для сериализации foreach ($k in @($o.Keys)) { if ($o[$k] -is [System.Collections.ArrayList]) { $o[$k] = @($o[$k]) } } return $o } function Build-ChartSettings { param($setNode) return (Build-ChartNode $setNode '') } # --- 5. Form-level assembly --- $dsl = [ordered]@{} $titleNode = $root.SelectSingleNode("lf:Title", $ns) if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } } # properties (прямые скаляры под <Form>, PascalCase → camelCase) $KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance','ShowCloseButton','HorizontalAlign','ChildrenAlign','ShowTitle','ConversationsRepresentation','CollapseItemsByImportanceVariant','GroupList','ChildItemsWidth','VerticalAlign','HorizontalSpacing','CustomSettingsFolder','SettingsStorage','Enabled','Scale') $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 } } } # Ссылка на члена формы по id ("N:uuid") в groupList/customSettingsFolder НЕ воспроизводима: наш # компилятор переназначает id, и ссылка указала бы не туда (либо вовсе dangling — платформа сама их # часто не разрешает). Резолв в имя ненадёжен (N не всегда соответствует named-элементу). Игнорируем # с предупреждением (имя задаётся вручную через form-edit). Имя-форма захватывается как есть. foreach ($refKey in 'groupList','customSettingsFolder') { if ($props.Contains($refKey) -and "$($props[$refKey])" -match '^\d+:[0-9a-fA-F]{8}-') { [Console]::Error.WriteLine("form-decompile: $refKey = '$($props[$refKey])' — ссылка на члена формы по id, не воспроизводима (id переназначаются), опущена. Задайте по имени через form-edit.") $props.Remove($refKey) } } # 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' $acbDI = $acb.GetAttribute("DisplayImportance") # адаптивная важность форменной панели (атрибут тега) $acbKids = Decompile-Children $acb $acbObj = $null if ($haln -or ($acbAutofill -eq 'false') -or $acbKids -or $acbDI) { $acbObj = [ordered]@{} $acbObj['autoCmdBar'] = $acb.GetAttribute("name") if ($acbDI) { $acbObj['displayImportance'] = $acbDI } if ($haln) { $acbObj['horizontalAlign'] = $haln } if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false } if ($acbKids) { $acbObj['children'] = $acbKids } } elseif ($formHasCmdBar -and $null -eq $acbAutofill) { # Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер $acbObj = [ordered]@{} $acbObj['autoCmdBar'] = $acb.GetAttribute("name") $acbObj['autofill'] = $true } if ($acbObj) { [void]$elemList.Add($acbObj) } } if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } } if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) } # attributes # Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3. function Test-IsObjectLikeTypeDec([string]$type) { if ([string]::IsNullOrEmpty($type)) { return $false } if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true } return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.') } $attrsNode = $root.SelectSingleNode("lf:Attributes", $ns) if ($attrsNode) { $attrs = New-Object System.Collections.ArrayList # Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно # один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет) # → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно. $allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns)) $anyMainAttr = $false; $objLikeNodes = @() foreach ($an in $allAttrNodes) { if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true } $atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns)) if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an } } $suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null } foreach ($a in $allAttrNodes) { $ao = [ordered]@{} $ao['name'] = $a.GetAttribute("name") $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } # valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList # (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно. $setNode = $a.SelectSingleNode("lf:Settings", $ns) if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') { $vt = Decompile-Type $setNode $ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер "" } # Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват). elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') { $ao['planner'] = Build-PlannerSettings $setNode } # Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart"> → chart (генерик). elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'd4p1:(Gantt)?Chart$') { $ao['chart'] = Build-ChartSettings $setNode } if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false } $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } # Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок # из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем # ключ (компилятор воспроизведёт); иначе → явный заголовок. $isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title $tNode = $a.SelectSingleNode("lf:Title", $ns) if ($tNode) { $t = Get-LangTextWS $tNode # восстановление значимого пробела (whitespace-заголовок реквизита) 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) { # Снимаем префикс "имя." ТОЛЬКО когда остаток — простое подполе, которое компилятор # реинъектит (зеркало его условия: ≠имя, без точки, не UUID-ссылка ^N/M). Многоуровневый # путь "имя.Settings.Filter" и UUID-ссылка "имя.1/0:GUID" компилятор эмитит как есть # (префикс не вернёт) → храним ПОЛНЫЙ путь, иначе префикс теряется. $stripped = @($flds | ForEach-Object { if ($_ -match "^$([regex]::Escape($nm))\.([^.]+)$" -and $matches[1] -notmatch '^\d+/\d+') { $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 } # GetInvisibleFieldPresentations — дефолт true, платформа эмитит только отклонение (false, корпус 20/20). Факт. значение. $gifp = Get-Child $setNode 'GetInvisibleFieldPresentations'; if ($null -ne $gifp) { $so['getInvisibleFieldPresentations'] = ($gifp -eq 'true') } # Ключ набора (query-based список): KeyType (RowNumber/FieldValue/RowKey) + KeyField* (0+). $kt = Get-Child $setNode 'KeyType'; if ($kt) { $so['keyType'] = $kt } $kfNodes = @($setNode.SelectNodes("lf:KeyField", $ns) | ForEach-Object { $_.InnerText }) if ($kfNodes.Count -gt 0) { $so['keyFields'] = @($kfNodes) } # AutoSaveUserSettings — авто-сохранение польз. настроек дин-списка (в корпусе только false; # дефолт true → платформа эмитит отклонение). Захват факт. значения. $asus = Get-Child $setNode 'AutoSaveUserSettings'; if ($null -ne $asus) { $so['autoSaveUserSettings'] = ($asus -eq 'true') } $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns) $hasQ = [bool]($qtNode -and $qtNode.InnerText) if ($hasQ) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" } # ManualQuery: компилятор выводит из наличия query (hasQuery → true). Платформа в редких # случаях (корпус 16) хранит QueryText при ManualQuery=false → фиксируем отклонение от эвристики. $mqV = Get-Child $setNode 'ManualQuery' if ($null -ne $mqV) { $mqActual = ($mqV -eq 'true'); if ($mqActual -ne $hasQ) { $so['manualQuery'] = $mqActual } } # 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 ((Has-Child $fn 'dataPath') -and $dp -ne $fld) { $fo['dataPath'] = $dp } # Тип поля набора: DataSetFieldField (дефолт, компилятор хардкодит) vs # DataSetFieldNestedDataSet (поле-вложенный набор = реквизит табличной части). Маркер nested. $fTypeAttr = $fn.GetAttribute("type", $NS_XSI) if ($fTypeAttr -match 'NestedDataSet$') { $fo['nested'] = $true } elseif ($fTypeAttr -match 'Folder$') { $fo['folder'] = $true } $ftn = $fn.SelectSingleNode("dcssch:title", $ns) if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } } # valueType поля набора (тип значения; вычисляемые/кастомные поля). Грамматика типа. $fvt = $fn.SelectSingleNode("dcssch:valueType", $ns) if ($fvt) { $fvtVal = Decompile-Type $fvt; if ($fvtVal) { $fo['valueType'] = $fvtVal } } # presentationExpression (выражение представления поля) + appearance (формат/оформление поля) $fpe = Get-Child $fn 'presentationExpression' if ($null -ne $fpe -and $fpe -ne '') { $fo['presentationExpression'] = $fpe } $fappNode = $fn.SelectSingleNode("dcssch:appearance", $ns) if ($fappNode) { $fap = Get-SettingsAppearance $fappNode; if ($fap -and $fap.Count -gt 0) { $fo['appearance'] = $fap } } $furNode = $fn.SelectSingleNode("dcssch:useRestriction", $ns) if ($furNode) { $fur = Build-RestrictObj $furNode; if ($fur.Count -gt 0) { $fo['useRestriction'] = $fur } } $faurNode = $fn.SelectSingleNode("dcssch:attributeUseRestriction", $ns) if ($faurNode) { $faur = Build-RestrictObj $faurNode; if ($faur.Count -gt 0) { $fo['attributeUseRestriction'] = $faur } } $fipNode = $fn.SelectSingleNode("dcssch:inputParameters", $ns) if ($fipNode) { $fip = Build-DLInputParameters $fipNode; if (@($fip).Count -gt 0) { $fo['inputParameters'] = $fip } } [void]$fields.Add($fo) } $so['fields'] = @($fields) } # Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*. $calcNodes = @($setNode.SelectNodes("lf:CalculatedField", $ns)) if ($calcNodes.Count -gt 0) { $cfs = New-Object System.Collections.ArrayList foreach ($cn in $calcNodes) { [void]$cfs.Add((Build-CalcField $cn)) } $so['calculatedFields'] = @($cfs) } # Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings) $paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns)) if ($paramNodes.Count -gt 0) { $dlPars = New-Object System.Collections.ArrayList foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) } $so['parameters'] = @($dlPars) } # ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор # регенерит каноничный скелет. Захватываем только контейнеры с реальными # dcsset:item (filter/order/conditionalAppearance) в формат компилятора. $lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns) if ($lsNode) { $fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns) if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) { $flt = @() foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { $bi = (Build-FilterItem -itemNode $fc -loc "settings/filter") if ($null -ne $bi) { $flt += $bi } } if ($flt.Count -gt 0) { $so['filter'] = @($flt) } } $oNode = $lsNode.SelectSingleNode("dcsset:order", $ns) if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) { $ord = Build-Order -ordNode $oNode -loc "settings/order" if (@($ord).Count -gt 0) { $so['order'] = @($ord) } } $caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns) if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) { $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance" if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) } } # Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в # настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters. $dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns) if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) { $dp = Build-FormDataParameters $dpNode if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) } } # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). # Канон → $null (компилятор регенерит полный скелет, как раньше). # Группировка строк списка: прямой <dcsset:item> ListSettings (не в filter/order/CA). $grpItemNode = $lsNode.SelectSingleNode("dcsset:item", $ns) $grouping = $null if ($grpItemNode) { $grouping = Build-ListGrouping $grpItemNode } if ($null -ne $grouping) { $so['grouping'] = $grouping } $lsShape = Get-ListSettingsShape $lsNode ($null -ne $grouping) if ($null -ne $lsShape) { $so['listSettings'] = $lsShape } } if ($so.Count -gt 0) { $ao['settings'] = $so } } [void]$attrs.Add($ao) } if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) } } # conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>; # та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance) if ($attrsNode) { $caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns) if ($caNode) { $ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance" if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) } } } # parameters $parsNode = $root.SelectSingleNode("lf:Parameters", $ns) if ($parsNode) { $pars = New-Object System.Collections.ArrayList foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) { $po = [ordered]@{}; $po['name'] = $p.GetAttribute("name") $ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty } if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true } [void]$pars.Add($po) } if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) } } # commands $cmdsNode = $root.SelectSingleNode("lf:Commands", $ns) if ($cmdsNode) { $cmds = New-Object System.Collections.ArrayList foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) { $co = [ordered]@{}; $co['name'] = $c.GetAttribute("name") $act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act } if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true } # Заголовок команды: есть <Title> → захват; нет → суппресс-маркер "" (иначе компилятор # додумает из имени — авто-вывод неверен для ~0.13% команд без заголовка в оригинале). $tNode = $c.SelectSingleNode("lf:Title", $ns) if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } else { $co['title'] = '' } $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } $cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo } $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } # Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>) $ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate } $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } Set-CommandPicture $co $c $rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep } [void]$cmds.Add($co) } if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) } } # commandInterface (форменный <CommandInterface> — последний дочерний Form) $ci = Decompile-CommandInterface if ($null -ne $ci) { $dsl['commandInterface'] = $ci } # --- 6. Output --- $json = ConvertTo-CompactJson -obj $dsl if ($OutputPath) { [System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false))) Save-QueryFiles Write-Host "form-decompile: $OutputPath" } else { Write-Output $json }