Files
cc-1c-skills/.codeassistant/skills/form-decompile/scripts/form-decompile.ps1
T
2026-06-14 11:08:54 +00:00

2959 lines
168 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <v8:content> (декорации-распорки без 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)> не <Form> — это не управляемая форма.")
exit 2
}
# --- 1. Namespaces ---
$NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
$NS_V8 = "http://v8.1c.ru/8.1/data/core"
$NS_XR = "http://v8.1c.ru/8.3/xcf/readable"
$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings"
$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema"
$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core"
$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
$NS_APP = "http://v8.1c.ru/8.2/managed-application/core"
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("lf", $NS_LF)
$ns.AddNamespace("v8", $NS_V8)
$ns.AddNamespace("xr", $NS_XR)
$ns.AddNamespace("xsi", $NS_XSI)
$ns.AddNamespace("dcsset", $NS_DCSSET)
$ns.AddNamespace("dcssch", $NS_DCSSCH)
$ns.AddNamespace("dcscor", $NS_DCSCOR)
$ns.AddNamespace("v8ui", $NS_V8UI)
$ns.AddNamespace("app", $NS_APP)
# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе,
# компилятор регенерит тот же скелет → чистый раундтрип.
$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) ---
$script:outputDir = $null
$script:outputBasename = $null
if ($OutputPath) {
$od = Split-Path -Parent $OutputPath
if (-not $od) { $od = (Get-Location).Path }
$script:outputDir = $od
$script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath)
}
$script:queryFilesAccumulator = @()
$script:queryFileNamesUsed = @{}
# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file".
function Maybe-ExternalizeQuery {
param([string]$queryText, [string]$listName)
if (-not $queryText) { return $queryText }
if (-not $script:outputDir) { return $queryText }
$lineCount = ([regex]::Matches($queryText, "`n")).Count + 1
if ($lineCount -lt 3) { return $queryText }
$safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' }
$prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' }
$fileName = "$prefix$safe.sql"
$suffix = 1
while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" }
$script:queryFileNamesUsed[$fileName] = $true
$script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText }
return "@$fileName"
}
function Save-QueryFiles {
if ($script:queryFilesAccumulator.Count -eq 0) { return }
if (-not $script:outputDir) { return }
$enc = New-Object System.Text.UTF8Encoding($false)
foreach ($qf in $script:queryFilesAccumulator) {
[System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc)
}
[Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)")
}
# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/
# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false:
# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness
# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже).
function Test-ListSettingsHasContent {
param($lsNode)
if (-not $lsNode) { return $false }
foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) {
$cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns)
if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true }
}
return $false
}
# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance →
# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма ==
# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы
# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора.
function Get-ListSettingsShape {
param($lsNode, [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'})"
# Контейнер может нести собственный <dcsset:userSettingPresentation> (кастомная подпись
# настройки) — сохраняем форму по 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
}
# Группировка строк динамического списка: цепочка <dcsset:item StructureItemGroup> (каждый уровень =
# один groupItems-field; вложенность через дочерний <dcsset:item>). Возвращает ПЛОСКИЙ массив уровней
# (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 конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… —
# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие
# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча.
foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) {
$st = $s.GetAttribute("type", $NS_XSI)
if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$' -and $st -notmatch 'd4p1:(Gantt)?Chart$') {
Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings"
}
# Chart/GanttChart с точками/осями: типизированные значения (xsi:type), xsi:nil и ML с
# префиксом d4p1: (а не v8:) генерик-движок не сохраняет → честный скип. Частые
# дашборд-диаграммы/гант (серии/легенда/оформление/шкала) поддержаны.
elseif ($st -match 'd4p1:(Gantt)?Chart$' -and ($s.OuterXml -match '<d4p1:\w+ xsi:type=' -or $s.OuterXml -match '<d4p1:\w+ xsi:nil=' -or $s.OuterXml -match '<d4p1:item[ >]')) {
Fail-Ring3 -kind "Attribute>Settings $st с точками/осями (типизированные значения/d4p1-ML)" -loc "Attribute/Settings"
}
}
# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) ---
function Convert-StringToJsonLiteral {
param([string]$s)
if ($null -eq $s) { return 'null' }
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append('"')
foreach ($ch in $s.ToCharArray()) {
$code = [int]$ch
if ($code -eq 0x22) { [void]$sb.Append('\"') }
elseif ($code -eq 0x5C) { [void]$sb.Append('\\') }
elseif ($code -eq 0x08) { [void]$sb.Append('\b') }
elseif ($code -eq 0x09) { [void]$sb.Append('\t') }
elseif ($code -eq 0x0A) { [void]$sb.Append('\n') }
elseif ($code -eq 0x0C) { [void]$sb.Append('\f') }
elseif ($code -eq 0x0D) { [void]$sb.Append('\r') }
elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
else { [void]$sb.Append($ch) }
}
[void]$sb.Append('"')
return $sb.ToString()
}
function Try-InlineJson {
param($obj)
if ($null -eq $obj) { return 'null' }
if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
}
if ($obj -is [System.Collections.IDictionary]) {
if ($obj.Count -eq 0) { return '{}' }
$parts = @()
foreach ($k in $obj.Keys) {
$v = Try-InlineJson $obj[$k]
if ($null -eq $v) { return $null }
$parts += "$(Convert-StringToJsonLiteral "$k"): $v"
}
return '{ ' + ($parts -join ', ') + ' }'
}
if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
$items = @($obj)
if ($items.Count -eq 0) { return '[]' }
$parts = @()
foreach ($it in $items) {
$v = Try-InlineJson $it
if ($null -eq $v) { return $null }
$parts += $v
}
return '[' + ($parts -join ', ') + ']'
}
return $null
}
function ConvertTo-CompactJson {
param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120)
$indent = $indentUnit * $depth
$childIndent = $indentUnit * ($depth + 1)
if ($null -eq $obj) { return 'null' }
if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
}
$isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList])
if ($isContainer) {
$inlineAttempt = Try-InlineJson $obj
if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt }
}
if ($obj -is [System.Collections.IDictionary]) {
$keys = @($obj.Keys)
if ($keys.Count -eq 0) { return '{}' }
$parts = @()
foreach ($k in $keys) {
$val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit
$parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val"
}
return "{`n" + ($parts -join ",`n") + "`n$indent}"
}
if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
$items = @($obj)
if ($items.Count -eq 0) { return '[]' }
$parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" })
return "[`n" + ($parts -join ",`n") + "`n$indent]"
}
return (Convert-StringToJsonLiteral "$obj")
}
# --- 2. Helpers ---
# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей.
# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы
# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions).
$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar')
# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...}
function Get-LangText {
param($node)
if ($null -eq $node) { return $null }
$items = @($node.SelectNodes("v8:item", $ns))
if ($items.Count -eq 0) { return $null }
$map = [ordered]@{}
foreach ($it in $items) {
$lang = $it.SelectSingleNode("v8:lang", $ns)
$content = $it.SelectSingleNode("v8:content", $ns)
if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } }
}
if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] }
return $map
}
# Get-LangText с восстановлением значимого пробела: PreserveWhitespace=false стрипает
# <v8:content> </v8:content> → "" (неотличимо от суппресса). Платформа НЕ эмитит пустой
# Title/ToolTip, значит исходно был пробел → возвращаем " " (как Get-MLFormattedValue).
# Покрывает и одиночную строку (ru-only), и мультиязычную мапу (напр. декорация-разделитель
# «Пробел» с ru+en пробелами): восстанавливаем " " в каждом языке, где content-узел есть, но пуст.
# Точное число пробелов whitespace-only <v8:content> из 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 восстанавливает значимый пробел в <v8:content> </v8:content> (одиночный и
# мультиязычный случай) — иначе декорация-разделитель «Пробел» спуталась бы с суппресс-маркером "".
$text = Get-LangTextWS $titleNode
if ($null -eq $text) { return $null }
$fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true')
if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text }
$o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o
}
# Прочитать дочерний скаляр (по local-name, без namespace)
function Get-Child {
param($node, [string]$name)
$c = $node.SelectSingleNode("*[local-name()='$name']")
if ($c) { return $c.InnerText } else { return $null }
}
function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") }
function To-Bool { param([string]$v) return ($v -eq 'true') }
# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка).
# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type.
function Convert-TypedValue {
param([string]$raw, [string]$xsiType)
switch -regex ($xsiType) {
'decimal$' {
if ($raw -match '^-?\d+$') { return [int]$raw }
return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture)
}
'boolean$' { return ($raw -eq 'true') }
default { return $raw }
}
}
# =====================================================================
# Захват настроек компоновщика динамического списка (ListSettings):
# filter / order / conditionalAppearance. Логика портирована из навыка
# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance
# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd
# заменён на запись в stderr + пропуск элемента (form-decompile — draft,
# скрипт не падает на непокрытых конструкциях).
# =====================================================================
# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text.
function Get-Text {
param($node, [string]$xpath)
if (-not $node) { return $null }
if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText }
$n = $node.SelectSingleNode($xpath, $ns)
if ($n) { return $n.InnerText } else { return $null }
}
# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash.
# Алиас на уже существующий Get-LangText (тот же контракт).
function Get-MLText { param($node) return (Get-LangText $node) }
# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string.
# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText.
function Get-PresText {
param($node)
if (-not $node) { return $null }
$ml = Get-MLText $node
if ($null -ne $ml) { return $ml }
if ($node.InnerText) { return $node.InnerText }
return $null
}
# 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
}
# Прочитать <dcscor:value> в JSON-значение: Font/Line/Field/multilang/raw text.
function Read-AppearanceValueNode {
param($valNode)
if (-not $valNode) { return $null }
$vt = Get-LocalXsiType $valNode
if ($vt -eq 'LocalStringType') {
# НЕ схлопываем одноязычный в строку: значение параметра оформления различает
# xs:string (плоская строка) и LocalStringType (локализуемый текст) — обе формы
# одноязычно дают одну строку. Всегда объект-карта языков → компилятор эмитит LocalStringType.
$map = [ordered]@{}
foreach ($it in @($valNode.SelectNodes("v8:item", $ns))) {
$lang = $it.SelectSingleNode("v8:lang", $ns); $content = $it.SelectSingleNode("v8:content", $ns)
if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } }
}
return $map
}
if ($vt -eq 'Font') { return (Get-FontValue $valNode) }
if ($vt -eq 'Line') { return (Get-LineValue $valNode) }
# dcscor:Field — значение = ссылка на поле компоновки → объект {field:путь}
if ($vt -eq 'Field') { return [ordered]@{ field = $valNode.InnerText } }
return $valNode.InnerText
}
# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd).
$script:filterOpMap = @{
'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>=';
'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn';
'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy';
'Contains'='contains'; 'NotContains'='notContains';
'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith';
'Like'='like'; 'NotLike'='notLike';
'Filled'='filled'; 'NotFilled'='notFilled'
}
# Render filter value node → shorthand-acceptable scalar string
function Get-FilterValue {
param($valNode)
if (-not $valNode) { return '_' }
$nil = $valNode.GetAttribute("nil", $NS_XSI)
if ($nil -eq 'true') { return '_' }
$vType = Get-LocalXsiType $valNode
if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText }
if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) }
$txt = $valNode.InnerText
if (-not $txt) { return '_' }
return $txt
}
# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field).
function Get-FilterValueWithType {
param($valNode)
if (-not $valNode) { return @{ value = '_'; type = $null } }
$rawType = $valNode.GetAttribute("type", $NS_XSI)
$nil = $valNode.GetAttribute("nil", $NS_XSI)
if ($nil -eq 'true') { return @{ value = '_'; type = $null } }
$vType = Get-LocalXsiType $valNode
if ($vType -eq 'LocalStringType') {
return @{ value = (Get-MLText $valNode); type = $rawType }
}
# Стандартная дата начала/окончания. Формы (от самой компактной):
# SBD Custom+date → голая ISO-дата без valueType (компилятор выводит SBD Custom — дефолт
# даты в фильтре, корпус 268 vs 2 xs:dateTime); именованный вариант → строка + valueType;
# SED Custom / нетипичное → объект {variant, date} + valueType. Иначе InnerText склеивал.
if ($vType -eq 'StandardBeginningDate' -or $vType -eq 'StandardEndDate') {
$variantN = $valNode.SelectSingleNode("v8:variant", $ns)
$dateN = $valNode.SelectSingleNode("v8:date", $ns)
$variantStr = if ($variantN) { $variantN.InnerText } else { '' }
if ($dateN) {
if ($vType -eq 'StandardBeginningDate' -and $variantStr -eq 'Custom') {
return @{ value = $dateN.InnerText; type = $null } # голая дата = SBD Custom шорткат
}
return @{ value = [ordered]@{ variant = $variantStr; date = $dateN.InnerText }; type = $rawType }
}
return @{ value = $variantStr; type = $rawType } # именованный вариант
}
$txt = $valNode.InnerText
if (-not $txt) { return @{ value = '_'; type = $rawType } }
if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } }
if ($vType -eq 'decimal') {
if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } }
return @{ value = [double]$txt; type = $rawType }
}
return @{ value = $txt; type = $rawType }
}
# Convert filter item node → shorthand string или object form (рекурсивно для групп).
function Build-FilterItem {
param($itemNode, [string]$loc)
$xtype = Get-LocalXsiType $itemNode
if ($xtype -eq 'FilterItemGroup') {
$gt = Get-Text $itemNode "dcsset:groupType"
$groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } }
$items = @()
foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) {
$bi = (Build-FilterItem -itemNode $c -loc "$loc/item")
if ($null -ne $bi) { $items += $bi }
}
$gObj = [ordered]@{ group = $groupName; items = $items }
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 (<dcsset:right xsi:type="xs:string"/>) ≠ отсутствие <right>:
# Get-FilterValueWithType маппит наличие пустого/nil right в value='_' (отсутствие → $null).
# Shorthand `_` неоднозначен (схлопывает оба, компилятор → нет right). Пробельные значения рвут
# shorthand-парсинг (split по пробелам). → форсим объектную форму (value='_' → self-closing;
# пробелы — как есть в <right>).
$valNeedsObj = $false
if ($rightNodes.Count -eq 1 -and -not $valueIsArrayFlag -and $op -notin $noValueOps) {
if ("$value" -eq '_') {
# Truly-empty <right xsi:type="xs:string"/> ИЛИ whitespace-only <right> </right>
# (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 из <dcsset:appearance> (Line/Font/multilang/nested items).
function Get-SettingsAppearance {
param($appNode)
if (-not $appNode) { return $null }
$dict = [ordered]@{}
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
$pName = Get-Text $it "dcscor:parameter"
$val = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $pName -or -not $val) { continue }
$rawVal = Read-AppearanceValueNode $val
$useV = Get-Text $it "dcscor:use"
$nestedItems = [ordered]@{}
foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
$subName = Get-Text $sub "dcscor:parameter"
$subVal = $sub.SelectSingleNode("dcscor:value", $ns)
if (-not $subName) { continue }
$subRaw = Read-AppearanceValueNode $subVal
$subUse = Get-Text $sub "dcscor:use"
$subEntry = [ordered]@{ value = $subRaw }
if ($subUse -eq 'false') { $subEntry['use'] = $false }
$nestedItems[$subName] = $subEntry
}
$valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line')
if ($valIsLine) {
if ($useV -eq 'false') { $rawVal['use'] = $false }
if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems }
$dict[$pName] = $rawVal
} elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) {
$wrap = [ordered]@{ value = $rawVal }
if ($useV -eq 'false') { $wrap['use'] = $false }
if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems }
$dict[$pName] = $wrap
} else {
$dict[$pName] = $rawVal
}
}
return $dict
}
# Build conditionalAppearance array.
function Build-ConditionalAppearance {
param($caNode, [string]$loc)
if (-not $caNode) { return @() }
$out = @()
$i = 0
foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) {
$entry = [ordered]@{}
$scopeNode = $it.SelectSingleNode("dcsset:scope", $ns)
if ($scopeNode -and $scopeNode.HasChildNodes) {
[Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)")
}
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection"
}
$filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$f = @()
foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) {
$bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter")
if ($null -ne $bi) { $f += $bi }
}
$entry['filter'] = $f
}
$appNode = $it.SelectSingleNode("dcsset:appearance", $ns)
$ap = Get-SettingsAppearance $appNode
if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap }
$presNode = $it.SelectSingleNode("dcsset:presentation", $ns)
if ($presNode) {
# Сохраняем форму по 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 тут — высота элемента (<Height>),
# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows.
function Add-Layout {
param($obj, $node)
# Общие свойства элемента (любой тип): default/drag/skip
if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true }
$soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') }
# EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet)
$esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') }
$edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') }
$fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm }
# AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault).
# Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики).
$amwNode = Get-Child $node 'AutoMaxWidth'
if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false }
elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true }
elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true }
$mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw }
if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false }
$mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh }
$w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w }
$h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h }
# Stretch: захват фактического значения (true И false — платформа эмитит явное)
$hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') }
$vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') }
$gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha }
$gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva }
$ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha }
# Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть»
foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) {
$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') }
}
$fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha }
$hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha }
# 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() }
}
# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика }
# в порядке документа. Имена обработчиков всегда явные (как у событий формы) —
# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим.
function Get-Events {
param($node, [string]$elName)
$ev = $node.SelectSingleNode("lf:Events", $ns)
if (-not $ev) { return $null }
$events = [ordered]@{}
foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) {
$events[$e.GetAttribute("name")] = $e.InnerText
}
if ($events.Count -eq 0) { return $null }
return $events
}
# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use).
# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }.
# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует.
function Decompile-XrFlag {
param($node, [string]$tag)
$el = $node.SelectSingleNode("*[local-name()='$tag']")
if (-not $el) { return $null }
$commonNode = $el.SelectSingleNode("*[local-name()='Common']")
$common = ($commonNode -and $commonNode.InnerText -eq 'true')
$valNodes = @($el.SelectNodes("*[local-name()='Value']"))
if ($valNodes.Count -eq 0) { return $common }
$roles = [ordered]@{}
foreach ($v in $valNodes) {
$rn = $v.GetAttribute("name")
if ($rn -match '^Role\.') { $rn = $rn.Substring(5) }
$roles[$rn] = ($v.InnerText -eq 'true')
}
$o = [ordered]@{}
$o['common'] = $common
$o['roles'] = $roles
return $o
}
# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel,
# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки).
# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index +
# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible).
# Голый элемент (только команда, Type=Auto) → строковый shorthand.
function Decompile-CommandInterface {
$ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns)
if (-not $ciNode) { return $null }
$ci = [ordered]@{}
foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) {
$pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns)
if (-not $pn) { continue }
$items = New-Object System.Collections.ArrayList
foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) {
$o = [ordered]@{}
$cmd = Get-Child $it 'Command'
$o['command'] = "$cmd"
$ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty }
$at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at }
$cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg }
$idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx }
$dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') }
$vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis }
# Голый элемент (только command) → строка-shorthand; иначе объект
if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) }
}
if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) }
}
if ($ci.Count -gt 0) { return $ci }
return $null
}
# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть).
function Decompile-FunctionalOptions {
param($node)
$foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns)
if (-not $foNode) { return $null }
$opts = New-Object System.Collections.ArrayList
foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) {
$t = $it.InnerText.Trim() -replace '^FunctionalOption\.', ''
[void]$opts.Add($t)
}
if ($opts.Count -gt 0) { return ,@($opts) }
return $null
}
# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions.
function Decompile-AttrColumn {
param($c)
$co = [ordered]@{}; $co['name'] = $c.GetAttribute("name")
$cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty }
$ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-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).
# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false.
# Источник: <xr:Ref> (именованная/стилевая) ИЛИ <xr:Abs> (встроенная → префикс "abs:").
# Скаляр (src) при loadTransparent=false и без TransparentPixel; иначе объект
# {src, loadTransparent?, transparentPixel?}.
function Get-PictureRef {
param($node, [string]$picTag)
$ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns)
$abs = $node.SelectSingleNode("lf:$picTag/xr:Abs", $ns)
if (-not $ref -and -not $abs) { return $null }
$src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" }
$lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns)
$ltTrue = ($lt -and $lt.InnerText -eq 'true')
$tpx = $node.SelectSingleNode("lf:$picTag/xr:TransparentPixel", $ns)
if (-not $ltTrue -and -not $tpx) { return $src }
$o = [ordered]@{ src = $src }
if ($ltTrue) { $o['loadTransparent'] = $true }
if ($tpx) { $o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } }
return $o
}
# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true (обратная конвенция к header/values):
# фиксируем только отклонение false. Источник <xr:Ref> или <xr:Abs> (→ "abs:"). При наличии
# <xr:TransparentPixel> → объектная форма {src, loadTransparent?, transparentPixel}, иначе скаляр picture
# + отдельный loadTransparent:false.
function Set-CommandPicture {
param($obj, $node)
$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns)
$abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns)
if (-not $ref -and -not $abs) { return }
$src = if ($ref) { $ref.InnerText } else { "abs:$($abs.InnerText)" }
$lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns)
$ltFalse = ($lt -and $lt.InnerText -eq 'false')
$tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns)
if ($tpx) {
$o = [ordered]@{ src = $src }
if ($ltFalse) { $o['loadTransparent'] = $false }
$o['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') }
$obj['picture'] = $o
} else {
$obj['picture'] = $src
if ($ltFalse) { $obj['loadTransparent'] = $false }
}
}
# Шрифт <Font ...> → строка-ref (если только ref+kind=StyleItem) или объект-атрибуты.
function Build-FontValue {
param($f)
$present = @()
foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
if ($f.HasAttribute($a)) { $present += $a }
}
# Чистый style-ref (ref + kind=StyleItem) → строка-шорткат
if ($present.Count -eq 2 -and ($present -contains 'ref') -and $f.GetAttribute('kind') -eq 'StyleItem') {
return $f.GetAttribute('ref')
}
$o = [ordered]@{}
foreach ($k in $present) {
$v = $f.GetAttribute($k)
if ($k -in @('height','scale') -and $v -match '^-?\d+$') { $o[$k] = [int]$v }
elseif ($k -in @('bold','italic','underline','strikeout')) { $o[$k] = ($v -eq 'true') }
else { $o[$k] = $v }
}
return $o
}
# Граница <Border> → строка-ref (из стиля) или объект {width, style}.
function Build-BorderValue {
param($b)
if ($b.HasAttribute('ref')) { return $b.GetAttribute('ref') }
$o = [ordered]@{}
if ($b.HasAttribute('width')) { $w = $b.GetAttribute('width'); $o['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } }
$st = $b.SelectSingleNode("v8ui:style", $ns)
if ($st) { $o['style'] = $st.InnerText }
return $o
}
# Оформление элемента (цвета/шрифты/граница) → canonical DSL-ключи. Цвет — verbatim-строка.
function Add-Appearance {
param($obj, $node)
$colorMap = @{
'TitleTextColor'='titleTextColor'; 'TitleBackColor'='titleBackColor'
'FooterTextColor'='footerTextColor'; 'FooterBackColor'='footerBackColor'
'TextColor'='textColor'; 'BackColor'='backColor'; 'BorderColor'='borderColor'
}
foreach ($tag in $colorMap.Keys) {
$c = $node.SelectSingleNode("lf:$tag", $ns)
if ($c) { $obj[$colorMap[$tag]] = $c.InnerText }
}
foreach ($pair in @(@('Font','font'), @('TitleFont','titleFont'), @('FooterFont','footerFont'))) {
$f = $node.SelectSingleNode("lf:$($pair[0])", $ns)
if ($f) { $obj[$pair[1]] = (Build-FontValue $f) }
}
$b = $node.SelectSingleNode("lf:Border", $ns)
if ($b) { $obj['border'] = (Build-BorderValue $b) }
}
function Add-CommonProps {
param($obj, $node, [string]$elName)
Add-Appearance $obj $node
if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true }
if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true }
if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true }
$uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv }
$titleNode = $node.SelectSingleNode("lf:Title", $ns)
if ($titleNode) {
$t = Get-LangTextWS $titleNode # восстановление значимого пробела (whitespace-заголовок)
if ($null -ne $t) { $obj['title'] = $t }
# formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост)
}
$ttNode = $node.SelectSingleNode("lf:ToolTip", $ns)
if ($ttNode) { $tt = Get-LangTextWS $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } }
$ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr }
# Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon)
$hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp }
$fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp }
$ev = Get-Events $node $elName
if ($ev) { $obj['events'] = $ev }
# CommandSet — общий для полей (input/label/check/spreadsheet/html/formatted/picture):
# список отключённых команд редактора. Только <ExcludedCommand>, пустого не бывает.
$csNode = $node.SelectSingleNode("lf:CommandSet", $ns)
if ($csNode) {
$exc = New-Object System.Collections.ArrayList
foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) }
if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) }
}
}
# --- 3. Type decompile (inverse of Emit-Type) ---
function Decompile-Type {
param($typeNode)
if (-not $typeNode) { return $null }
$parts = New-Object System.Collections.ArrayList
foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) {
$raw = $vt.InnerText.Trim()
$short = $raw
# break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList).
switch -regex ($raw) {
'^xs:string$' {
$len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns)
$al = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:AllowedLength", $ns)
$fixed = ($al -and $al.InnerText -eq 'Fixed') # Variable = дефолт (опускаем); Fixed — явно
if ($len -and [int]$len.InnerText -gt 0) {
$short = if ($fixed) { "string($($len.InnerText),fixed)" } else { "string($($len.InnerText))" }
} else { $short = "string" } # Length=0 → всегда Variable (корпус)
break
}
'^xs:decimal$' {
$d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns)
$f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns)
$sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns)
$dd = if ($d) { $d.InnerText } else { '0' }
$ff = if ($f) { $f.InnerText } else { '0' }
if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" }
break
}
'^xs:boolean$' { $short = "boolean"; break }
'^xs:dateTime$' {
$df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns)
$dfv = if ($df) { $df.InnerText } else { 'DateTime' }
switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } }
break
}
'^cfg:(.+)$' { $short = $matches[1]; break }
'^(v8|v8ui):' {
# Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом
# (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост.
$rev = @{
'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList'
'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal'
'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure'
'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font'
}
if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw }
break
}
default { $short = $raw }
}
[void]$parts.Add($short)
}
# TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида».
# Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена.
foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) {
$raw = $ts.InnerText.Trim()
$short = $raw -replace '^(v8ui|v8|cfg):', ''
[void]$parts.Add($short)
}
# TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так
# сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID глобально
# стабилен → эмитим verbatim как маркер 'typeid:GUID' (компилятор разворачивает обратно; как роль-по-GUID).
foreach ($ti in @($typeNode.SelectNodes("v8:TypeId", $ns))) {
[void]$parts.Add("typeid:" + $ti.InnerText.Trim())
}
if ($parts.Count -eq 0) { return $null }
if ($parts.Count -eq 1) { return $parts[0] }
return ($parts -join ' | ')
}
# Ограничения использования (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 динамического списка (<CalculatedField>) → объектная модель.
# Зеркало эмиссии 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-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии
# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и
# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter.
function Build-DLInputParameters {
param($ipNode)
$items = New-Object System.Collections.ArrayList
foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) {
$io = [ordered]@{}
$io['parameter'] = Get-Child $it 'parameter'
$useN = $it.SelectSingleNode("dcscor:use", $ns)
if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false }
$valN = $it.SelectSingleNode("dcscor:value", $ns)
if ($valN) {
$vt = $valN.GetAttribute("type", $NS_XSI)
if ($vt -match 'ChoiceParameters$') {
$cps = New-Object System.Collections.ArrayList
foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) {
$cpo = [ordered]@{}
$cpo['name'] = Get-Child $cpi 'choiceParameter'
$vals = New-Object System.Collections.ArrayList
foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) {
[void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI))))
}
$cpo['values'] = @($vals)
[void]$cps.Add($cpo)
}
$io['choiceParameters'] = @($cps)
} elseif ($vt -match 'ChoiceParameterLinks$') {
$cpls = New-Object System.Collections.ArrayList
foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) {
$cpo = [ordered]@{}
$cpo['name'] = Get-Child $cpi 'choiceParameter'
$cpo['value'] = Get-Child $cpi 'value'
$md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md }
[void]$cpls.Add($cpo)
}
$io['choiceParameterLinks'] = @($cpls)
} 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) — захватываем ВСЕ <dcscor:value> массивом
$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: платформа пишет <value xsi:nil/>
# не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит →
# явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.)
$vNodes = @($pNode.SelectNodes("dcssch:value", $ns))
if ($vNodes.Count -gt 1) {
# valueListAllowed: список значений — захватываем ВСЕ <dcssch:value> массивом
$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 (нет детей) → совпадает с дефолтом → молчим;
# голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта).
function Decompile-CompanionPanel {
param($node, [string]$tag, [bool]$isDynListTable = $false)
$p = $node.SelectSingleNode("lf:$tag", $ns)
if (-not $p) { return $null }
$autofillRaw = Get-Child $p 'Autofill'
$halign = Get-Child $p 'HorizontalAlign'
$kids = Decompile-Children $p
$hasKids = $kids -and @($kids).Count -gt 0
if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) {
if ($autofillRaw -eq 'false') { return $null } # = дефолт эвристики → молчим
return [ordered]@{ autofill = $true } # голая панель → отклонение
}
if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null }
$o = [ordered]@{}
if ($halign) { $o['horizontalAlign'] = $halign }
if ($autofillRaw -eq 'false') { $o['autofill'] = $false }
elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true }
if ($hasKids) { $o['children'] = $kids }
return $o
}
# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null.
# У RadioButtonField и InputField.
function Decompile-ChoiceList {
param($node)
$cl = $node.SelectSingleNode("lf:ChoiceList", $ns)
if (-not $cl) { return $null }
$items = New-Object System.Collections.ArrayList
foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) {
$valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns)
$presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns)
$ci = [ordered]@{}
if ($valNode) {
if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') {
# nil-значение элемента choiceList — компилятор эмитит <Value xsi:nil="true"/>
# (иначе 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/> → "" — суппресс-маркер,
# подавляет авто-вывод компилятора (иначе компилятор додумает 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
}
# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо
# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value).
function Convert-ChoiceParamValue {
param($valNode)
$vt = $valNode.GetAttribute("type", $NS_XSI)
if ($vt -match 'FixedArray$') {
$arr = New-Object System.Collections.ArrayList
foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) {
$inner = $it.SelectSingleNode("lf:Value", $ns)
if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) }
}
return ,@($arr)
}
return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt
}
# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}].
function Decompile-ChoiceParameters {
param($node)
$cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns)
if (-not $cpn) { return $null }
$items = New-Object System.Collections.ArrayList
foreach ($it in @($cpn.SelectNodes("app:item", $ns))) {
$o = [ordered]@{}
$o['name'] = $it.GetAttribute("name")
$valNode = $it.SelectSingleNode("app:value/lf:Value", $ns)
if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode }
[void]$items.Add($o)
}
if ($items.Count -gt 0) { return ,@($items) }
return $null
}
# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> →
# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит).
function Decompile-ChoiceParameterLinks {
param($node)
$cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns)
if (-not $cln) { return $null }
$items = New-Object System.Collections.ArrayList
foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) {
$o = [ordered]@{}
$o['name'] = Get-Text $lk "xr:Name"
$o['dataPath'] = Get-Text $lk "xr:DataPath"
$vc = Get-Text $lk "xr:ValueChange"
if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc }
[void]$items.Add($o)
}
if ($items.Count -gt 0) { return ,@($items) }
return $null
}
# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}.
function Decompile-TypeLink {
param($node)
$tn = $node.SelectSingleNode("lf:TypeLink", $ns)
if (-not $tn) { return $null }
$o = [ordered]@{}
$o['dataPath'] = Get-Text $tn "xr:DataPath"
$li = Get-Text $tn "xr:LinkItem"
if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li }
return $o
}
# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}).
function Add-FormatProps {
param($obj, $node)
$fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } }
$efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } }
}
# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation.
# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно).
function Add-AdditionCore {
param($obj, $node, [string]$elName)
$src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText }
Add-CommonProps $obj $node $elName
$hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() }
}
# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту
# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем.
function Decompile-TableAdditions {
param($tableNode, [string]$tableName)
$tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' }
$map = [ordered]@{}
foreach ($child in $tableNode.ChildNodes) {
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if (-not $tagToKey.ContainsKey($child.LocalName)) { continue }
$key = $tagToKey[$child.LocalName]
$nm = $child.GetAttribute("name")
$o = [ordered]@{}; $o[$key] = $nm
Add-AdditionCore $o $child $nm
Add-Layout $o $child
$o.Remove($key) # имя авто
if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') } # source=таблица дефолт
if ($o.Count -gt 0) { $map[$key] = $o }
}
if ($map.Count -gt 0) { return $map }
return $null
}
# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode).
# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост.
function Decompile-SimpleField {
param($obj, $node, [string]$name, [string]$key)
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
}
# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField).
function Add-GaugeScalars {
param($obj, $node, $tags)
foreach ($p in $tags) {
$v = Get-Child $node $p
if ($null -eq $v) { continue }
$key = $p.Substring(0,1).ToLower() + $p.Substring(1)
if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v }
}
}
function Decompile-Element {
param($node)
$tag = $node.LocalName
if (-not $ELEMENT_KEY.ContainsKey($tag)) {
Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag"
}
$key = $ELEMENT_KEY[$tag]
$name = $node.GetAttribute("name")
$obj = [ordered]@{}
switch ($tag) {
'UsualGroup' {
# group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем).
# group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление
# не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical'
# — иначе компилятор додумает <Group>Vertical</Group> там, где его нет.
$g = Get-Child $node 'Group'
$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical'; 'HorizontalIfPossible'='horizontalIfPossible' }
if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' }
$behavior = Get-Child $node 'Behavior'
if ($behavior) {
$bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' }
if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior }
}
$obj['name'] = $name
Add-CommonProps $obj $node $name
$rep = Get-Child $node 'Representation'
if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } }
$st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже)
$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 }
# Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы)
Add-FormatProps $obj $node
$kids = Decompile-Children $node
if ($kids) { $obj['children'] = $kids }
}
'ColumnGroup' {
# columnGroup = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление
# не эмитим). Иначе компилятор додумает <Group>Horizontal</Group> там, где его нет.
$g = Get-Child $node 'Group'
$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'InCell'='inCell' }
if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' }
$obj['name'] = $name
Add-CommonProps $obj $node $name
$st = Get-Child $node 'ShowTitle'; if ($null -ne $st) { $obj['showTitle'] = ($st -eq 'true') } # факт. значение (явный true тоже)
$sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) }
$kids = Decompile-Children $node
if ($kids) { $obj['children'] = $kids }
}
'InputField' {
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
# MultiLine: факт. значение (платформа эмитит и явный false — 425 в корпусе; ≠ «if true»)
$mlIn = Get-Child $node 'MultiLine'; if ($null -ne $mlIn) { $obj['multiLine'] = ($mlIn -eq 'true') }
# PasswordMode: факт. значение (платформа эмитит и false — 349/504 в корпусе; ≠ «if true»)
$pmIn = Get-Child $node 'PasswordMode'; if ($null -ne $pmIn) { $obj['passwordMode'] = ($pmIn -eq 'true') }
$mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') }
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
$ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-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 — типизированное (<MinValue xsi:type="xs:decimal">N). Тип кодируем в JSON-тип:
# xs:decimal/int → число (компилятор → xs:decimal), иначе → строка (компилятор → xs:string).
foreach ($p in @('MinValue','MaxValue')) {
$mn = $node.SelectSingleNode("lf:$p", $ns)
if ($mn) {
$xt = $mn.GetAttribute("type", $NS_XSI); $txt = $mn.InnerText
$key = $p.Substring(0,1).ToLower() + $p.Substring(1)
if ($xt -match 'decimal|int') {
if ($txt -match '^-?\d+$') { $obj[$key] = [int]$txt } elseif ($txt -match '^-?\d+\.\d+$') { $obj[$key] = [decimal]$txt } else { $obj[$key] = $txt }
} else { $obj[$key] = $txt }
}
}
# Ограничение доступных типов (поле на составном/характеристика-типе): домен типов + явный набор.
# availableTypes — тот же формат типа, что у реквизитов (§type), захват через Decompile-Type.
$tde = Get-Child $node 'TypeDomainEnabled'; if ($null -ne $tde) { $obj['typeDomainEnabled'] = (To-Bool $tde) }
$atNode = $node.SelectSingleNode("lf:AvailableTypes", $ns); if ($atNode) { $at = Decompile-Type $atNode; if ($at) { $obj['availableTypes'] = $at } }
$cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr }
$cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp }
if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false }
$cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl }
Add-FormatProps $obj $node
# Параметры выбора / Связи параметров выбора / Связь по типу
$cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp }
$cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl }
$tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk }
}
'CheckBoxField' {
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
# CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение
$cbt = Get-Child $node 'CheckBoxType'
if ($null -eq $cbt) { $obj['checkBoxType'] = '' }
elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) }
Add-TitleLocation $obj $node 'Right'
$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-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: тег <Hiperlink> (опечатка платформы), не <Hyperlink>
if ((Get-Child $node 'Hiperlink') -eq 'true') { $obj['hyperlink'] = $true }
# PasswordMode на LabelField — платформа эмитит явный false (редко); захват факт. значения
$pm = Get-Child $node 'PasswordMode'; if ($null -ne $pm) { $obj['passwordMode'] = ($pm -eq 'true') }
$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-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 (атрибут <Title formatted> у PictureDecoration)
$tiNode = $node.SelectSingleNode("lf:Title", $ns)
if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } }
$npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-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
}