mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-16 15:55:16 +03:00
f534add7b4
Расширение Phase 1 кластера Chart-Settings: реквизит pl:Planner теперь несёт измерения планировщика (<pl:dimension> — «Измерения» в конфигураторе) с элементами. DSL planner.dimensions[]: объект разреза (value — ссылка xr:DesignTimeRef или nil, text-заголовок, цвета, font) + elements[] (элементы измерения, РЕКУРСИВНЫ — могут нести вложенные elements, как показывает UI колонкой «Элементы»; поле showOnlySubordinatesAreas). Тип value авто-выводится: ссылочный вид → xsi:type="xr:DesignTimeRef", иначе xs:string. Пустой текст → самозакрывающийся <pl:text/> (как в выгрузке). Общие хелперы Emit/Get-PlannerValue/Text применены и к элементам расписания (items). Раундтрип бит-в-бит: синтетика upload/epf/Диаграммы (items + 2 dimensions + вложенные elements + period). Зеркало py (ps1==py байт-в-байт). Кейс chart-fields расширен измерением (nil-разрез + xs:string-элемент + showOnlySubordinatesAreas), сертифицирован загрузкой в 1С. Регресс 41/41 (ps1+py). Ограничение: item.dimensionValues (привязка элемента расписания к элементам измерений) пока всегда пустой. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2360 lines
123 KiB
PowerShell
2360 lines
123 KiB
PowerShell
# form-decompile v0.81 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||
param(
|
||
[Parameter(Mandatory)]
|
||
[Alias('Path')]
|
||
[string]$FormPath,
|
||
|
||
[string]$OutputPath
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||
|
||
# --- 0. Resolve and validate input ---
|
||
if (-not (Test-Path $FormPath)) {
|
||
Write-Error "Form not found: $FormPath"
|
||
exit 1
|
||
}
|
||
$FormPath = (Resolve-Path $FormPath).Path
|
||
|
||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||
$xmlDoc.PreserveWhitespace = $false
|
||
$xmlDoc.Load($FormPath)
|
||
$root = $xmlDoc.DocumentElement
|
||
|
||
# Ring 2: not a managed Form
|
||
if ($root.LocalName -ne 'Form') {
|
||
[Console]::Error.WriteLine("form-decompile: корневой элемент <$($root.LocalName)> не <Form> — это не управляемая форма.")
|
||
exit 2
|
||
}
|
||
|
||
# --- 1. Namespaces ---
|
||
$NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
|
||
$NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
||
$NS_XR = "http://v8.1c.ru/8.3/xcf/readable"
|
||
$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
||
|
||
$NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings"
|
||
$NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema"
|
||
$NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core"
|
||
$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
|
||
$NS_APP = "http://v8.1c.ru/8.2/managed-application/core"
|
||
|
||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||
$ns.AddNamespace("lf", $NS_LF)
|
||
$ns.AddNamespace("v8", $NS_V8)
|
||
$ns.AddNamespace("xr", $NS_XR)
|
||
$ns.AddNamespace("xsi", $NS_XSI)
|
||
$ns.AddNamespace("dcsset", $NS_DCSSET)
|
||
$ns.AddNamespace("dcssch", $NS_DCSSCH)
|
||
$ns.AddNamespace("dcscor", $NS_DCSCOR)
|
||
$ns.AddNamespace("v8ui", $NS_V8UI)
|
||
$ns.AddNamespace("app", $NS_APP)
|
||
|
||
# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
|
||
# Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе,
|
||
# компилятор регенерит тот же скелет → чистый раундтрип.
|
||
$CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
|
||
$CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
|
||
$CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
|
||
$CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
|
||
|
||
# --- Вынос запроса динсписка в .sql рядом с output (зеркало skd-decompile) ---
|
||
$script:outputDir = $null
|
||
$script:outputBasename = $null
|
||
if ($OutputPath) {
|
||
$od = Split-Path -Parent $OutputPath
|
||
if (-not $od) { $od = (Get-Location).Path }
|
||
$script:outputDir = $od
|
||
$script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath)
|
||
}
|
||
$script:queryFilesAccumulator = @()
|
||
$script:queryFileNamesUsed = @{}
|
||
|
||
# Запрос ≥3 строк + есть outputDir → вынести в `<basename>-<listName>.sql`, вернуть "@file".
|
||
function Maybe-ExternalizeQuery {
|
||
param([string]$queryText, [string]$listName)
|
||
if (-not $queryText) { return $queryText }
|
||
if (-not $script:outputDir) { return $queryText }
|
||
$lineCount = ([regex]::Matches($queryText, "`n")).Count + 1
|
||
if ($lineCount -lt 3) { return $queryText }
|
||
$safe = ($listName -replace '[^\w\-]', '_'); if (-not $safe) { $safe = 'query' }
|
||
$prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' }
|
||
$fileName = "$prefix$safe.sql"
|
||
$suffix = 1
|
||
while ($script:queryFileNamesUsed.ContainsKey($fileName)) { $suffix++; $fileName = "$prefix$safe`_$suffix.sql" }
|
||
$script:queryFileNamesUsed[$fileName] = $true
|
||
$script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText }
|
||
return "@$fileName"
|
||
}
|
||
function Save-QueryFiles {
|
||
if ($script:queryFilesAccumulator.Count -eq 0) { return }
|
||
if (-not $script:outputDir) { return }
|
||
$enc = New-Object System.Text.UTF8Encoding($false)
|
||
foreach ($qf in $script:queryFilesAccumulator) {
|
||
[System.IO.File]::WriteAllText((Join-Path $script:outputDir $qf.fileName), $qf.text, $enc)
|
||
}
|
||
[Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)")
|
||
}
|
||
|
||
# Есть ли в ListSettings содержательные настройки (реальные items фильтра/порядка/
|
||
# условного оформления/параметров)? Пустой скелет (только viewMode+GUID) → false:
|
||
# декомпилятор опускает настройки, компилятор регенерит каноничный скелет, harness
|
||
# нормализует GUID → чистый раундтрип. true → контент захватывается (см. ниже).
|
||
function Test-ListSettingsHasContent {
|
||
param($lsNode)
|
||
if (-not $lsNode) { return $false }
|
||
foreach ($cont in @('filter','order','conditionalAppearance','dataParameters')) {
|
||
$cn = $lsNode.SelectSingleNode("dcsset:$cont", $ns)
|
||
if ($cn -and $cn.SelectSingleNode("dcsset:item", $ns)) { return $true }
|
||
}
|
||
return $false
|
||
}
|
||
|
||
# Форма ListSettings: ordered-карта present top-level элементов (filter/order/conditionalAppearance →
|
||
# блок-мета 'v'/'u'/'vu'/''; itemsViewMode/itemsUserSettingID → $true). Возвращает $null, если форма ==
|
||
# полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы
|
||
# (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора.
|
||
function Get-ListSettingsShape {
|
||
param($lsNode)
|
||
if (-not $lsNode) { return $null }
|
||
$shape = [ordered]@{}
|
||
foreach ($child in $lsNode.ChildNodes) {
|
||
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||
$tag = $child.LocalName
|
||
if ($tag -in @('filter','order','conditionalAppearance')) {
|
||
$hasVM = $null -ne $child.SelectSingleNode("dcsset:viewMode", $ns)
|
||
$hasUS = $null -ne $child.SelectSingleNode("dcsset:userSettingID", $ns)
|
||
$shape[$tag] = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})"
|
||
} elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true }
|
||
elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true }
|
||
else { return $null } # item/dataParameters/itemsUserSettingPresentation/… → канон-fallback
|
||
}
|
||
# Полный каноничный скелет → опускаем (компилятор регенерит)
|
||
if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and `
|
||
$shape['conditionalAppearance'] -eq 'vu' -and $shape['itemsViewMode'] -eq $true -and $shape['itemsUserSettingID'] -eq $true) { return $null }
|
||
return $shape
|
||
}
|
||
|
||
# --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) ---
|
||
function Fail-Ring3 {
|
||
param([string]$kind, [string]$loc)
|
||
[Console]::Error.WriteLine("form-decompile: декомпиляция пока не поддерживает $kind (path: $loc)")
|
||
[Console]::Error.WriteLine("Для точечной работы с этой формой используй /form-edit.")
|
||
exit 3
|
||
}
|
||
# ConditionalAppearance со scope (привязка к области) пока не воспроизводим — fail-ring3 только в этом случае.
|
||
foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']/*[local-name()='item']/*[local-name()='scope'][node()]")) { Fail-Ring3 -kind "ConditionalAppearance со scope" -loc "form/ConditionalAppearance/item/scope" }
|
||
# Реквизит с design-time конфигурацией (<Settings> chart-типа: Диаграмма/ДиаграммаГанта/… —
|
||
# d4p1:GanttChart и т.п.). Поддержаны только TypeDescription (valueType) и DynamicList; прочие
|
||
# (встроенная конфигурация диаграммы) пока НЕ воспроизводим → честный скип, чтобы не потерять молча.
|
||
foreach ($s in $xmlDoc.SelectNodes("//*[local-name()='Attribute']/*[local-name()='Settings']")) {
|
||
$st = $s.GetAttribute("type", $NS_XSI)
|
||
if ($st -and $st -notmatch 'TypeDescription$' -and $st -notmatch 'DynamicList$' -and $st -notmatch 'Planner$') {
|
||
Fail-Ring3 -kind "Attribute>Settings типа '$st' (design-time конфигурация, напр. диаграмма)" -loc "Attribute/Settings"
|
||
}
|
||
}
|
||
|
||
# --- 1c. Compact JSON serializer (созвучно skd-decompile: 2-проб. indent, inline в пределах lineLimit) ---
|
||
function Convert-StringToJsonLiteral {
|
||
param([string]$s)
|
||
if ($null -eq $s) { return 'null' }
|
||
$sb = New-Object System.Text.StringBuilder
|
||
[void]$sb.Append('"')
|
||
foreach ($ch in $s.ToCharArray()) {
|
||
$code = [int]$ch
|
||
if ($code -eq 0x22) { [void]$sb.Append('\"') }
|
||
elseif ($code -eq 0x5C) { [void]$sb.Append('\\') }
|
||
elseif ($code -eq 0x08) { [void]$sb.Append('\b') }
|
||
elseif ($code -eq 0x09) { [void]$sb.Append('\t') }
|
||
elseif ($code -eq 0x0A) { [void]$sb.Append('\n') }
|
||
elseif ($code -eq 0x0C) { [void]$sb.Append('\f') }
|
||
elseif ($code -eq 0x0D) { [void]$sb.Append('\r') }
|
||
elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
|
||
else { [void]$sb.Append($ch) }
|
||
}
|
||
[void]$sb.Append('"')
|
||
return $sb.ToString()
|
||
}
|
||
function Try-InlineJson {
|
||
param($obj)
|
||
if ($null -eq $obj) { return 'null' }
|
||
if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
|
||
if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
|
||
if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
|
||
if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
|
||
return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
|
||
}
|
||
if ($obj -is [System.Collections.IDictionary]) {
|
||
if ($obj.Count -eq 0) { return '{}' }
|
||
$parts = @()
|
||
foreach ($k in $obj.Keys) {
|
||
$v = Try-InlineJson $obj[$k]
|
||
if ($null -eq $v) { return $null }
|
||
$parts += "$(Convert-StringToJsonLiteral "$k"): $v"
|
||
}
|
||
return '{ ' + ($parts -join ', ') + ' }'
|
||
}
|
||
if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
|
||
$items = @($obj)
|
||
if ($items.Count -eq 0) { return '[]' }
|
||
$parts = @()
|
||
foreach ($it in $items) {
|
||
$v = Try-InlineJson $it
|
||
if ($null -eq $v) { return $null }
|
||
$parts += $v
|
||
}
|
||
return '[' + ($parts -join ', ') + ']'
|
||
}
|
||
return $null
|
||
}
|
||
function ConvertTo-CompactJson {
|
||
param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 120)
|
||
$indent = $indentUnit * $depth
|
||
$childIndent = $indentUnit * ($depth + 1)
|
||
if ($null -eq $obj) { return 'null' }
|
||
if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
|
||
if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
|
||
if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
|
||
if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
|
||
return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
|
||
}
|
||
$isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList])
|
||
if ($isContainer) {
|
||
$inlineAttempt = Try-InlineJson $obj
|
||
if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) { return $inlineAttempt }
|
||
}
|
||
if ($obj -is [System.Collections.IDictionary]) {
|
||
$keys = @($obj.Keys)
|
||
if ($keys.Count -eq 0) { return '{}' }
|
||
$parts = @()
|
||
foreach ($k in $keys) {
|
||
$val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit
|
||
$parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val"
|
||
}
|
||
return "{`n" + ($parts -join ",`n") + "`n$indent}"
|
||
}
|
||
if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
|
||
$items = @($obj)
|
||
if ($items.Count -eq 0) { return '[]' }
|
||
$parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" })
|
||
return "[`n" + ($parts -join ",`n") + "`n$indent]"
|
||
}
|
||
return (Convert-StringToJsonLiteral "$obj")
|
||
}
|
||
|
||
# --- 2. Helpers ---
|
||
|
||
# Companion-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей.
|
||
# Дополнения (Search*/ViewStatus) БОЛЬШЕ не companion — декомпилируются как тип-элементы
|
||
# (кастомные в AutoCommandBar/ChildItems → commandBar.children; стандартные на уровне таблицы → карта additions).
|
||
$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar')
|
||
|
||
# Извлечь мультиязычный Title/Presentation → string (ru) или ordered hash {ru,en,...}
|
||
function Get-LangText {
|
||
param($node)
|
||
if ($null -eq $node) { return $null }
|
||
$items = @($node.SelectNodes("v8:item", $ns))
|
||
if ($items.Count -eq 0) { return $null }
|
||
$map = [ordered]@{}
|
||
foreach ($it in $items) {
|
||
$lang = $it.SelectSingleNode("v8:lang", $ns)
|
||
$content = $it.SelectSingleNode("v8:content", $ns)
|
||
if ($lang) { $map[$lang.InnerText] = if ($content) { $content.InnerText } else { "" } }
|
||
}
|
||
if ($map.Count -eq 1 -and $map.Contains('ru')) { return $map['ru'] }
|
||
return $map
|
||
}
|
||
|
||
# Авто-вывод заголовка из имени — ТОЧНОЕ зеркало Title-FromName из form-compile.
|
||
# Нужен, чтобы опускать ru-only заголовки, которые компилятор воспроизведёт сам.
|
||
function Title-FromName {
|
||
param([string]$name)
|
||
if (-not $name) { return '' }
|
||
$s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2')
|
||
$s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2')
|
||
$parts = $s -split ' '
|
||
if ($parts.Count -eq 0) { return $s }
|
||
$out = New-Object System.Collections.ArrayList
|
||
[void]$out.Add($parts[0])
|
||
for ($i = 1; $i -lt $parts.Count; $i++) {
|
||
$p = $parts[$i]
|
||
if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) { [void]$out.Add($p) }
|
||
else { [void]$out.Add($p.ToLower()) }
|
||
}
|
||
return ($out -join ' ')
|
||
}
|
||
|
||
# Детектор «настоящей» inline-разметки форматированного текста (идентичен form-compile!).
|
||
$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)'
|
||
function Test-HasRealMarkup {
|
||
param($text)
|
||
if ($null -eq $text) { return $false }
|
||
$vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) } else { @("$text") }
|
||
foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } }
|
||
return $false
|
||
}
|
||
# Title-узел → DSL-значение ML-поля (гибрид): строка/мапа когда авто-детект formatted
|
||
# совпал с атрибутом; иначе явный {text, formatted}.
|
||
function Get-MLFormattedValue {
|
||
param($titleNode)
|
||
if (-not $titleNode) { return $null }
|
||
$text = Get-LangText $titleNode
|
||
if ($null -eq $text) { return $null }
|
||
# Whitespace-only заголовок (декорация-разделитель): PreserveWhitespace=false стрипает значимый
|
||
# пробел в <v8:content> </v8:content> → пустой текст при наличии узла content. Платформа НЕ
|
||
# эмитит пустой Title, значит исходно был пробел — восстанавливаем (иначе спутаем с суппресс-маркером "").
|
||
if ($text -is [string] -and $text -eq '' -and $titleNode.SelectSingleNode("v8:item/v8:content", $ns)) { $text = ' ' }
|
||
$fmtAttr = ($titleNode.GetAttribute('formatted') -eq 'true')
|
||
if ($fmtAttr -eq (Test-HasRealMarkup $text)) { return $text }
|
||
$o = [ordered]@{}; $o['text'] = $text; $o['formatted'] = $fmtAttr; return $o
|
||
}
|
||
|
||
# Прочитать дочерний скаляр (по local-name, без namespace)
|
||
function Get-Child {
|
||
param($node, [string]$name)
|
||
$c = $node.SelectSingleNode("*[local-name()='$name']")
|
||
if ($c) { return $c.InnerText } else { return $null }
|
||
}
|
||
function Has-Child { param($node, [string]$name) return $null -ne $node.SelectSingleNode("*[local-name()='$name']") }
|
||
function To-Bool { param([string]$v) return ($v -eq 'true') }
|
||
|
||
# Значение с учётом xsi:type → нативный JSON-тип (число/булево/строка).
|
||
# Нужно, чтобы авто-детект типа в компиляторе восстановил тот же xsi:type.
|
||
function Convert-TypedValue {
|
||
param([string]$raw, [string]$xsiType)
|
||
switch -regex ($xsiType) {
|
||
'decimal$' {
|
||
if ($raw -match '^-?\d+$') { return [int]$raw }
|
||
return [double]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture)
|
||
}
|
||
'boolean$' { return ($raw -eq 'true') }
|
||
default { return $raw }
|
||
}
|
||
}
|
||
|
||
# =====================================================================
|
||
# Захват настроек компоновщика динамического списка (ListSettings):
|
||
# filter / order / conditionalAppearance. Логика портирована из навыка
|
||
# skd-decompile (Build-FilterItem/Build-Order/Build-ConditionalAppearance
|
||
# и сериализаторы оформления). Механизм New-Sentinel/Add-Warning из skd
|
||
# заменён на запись в stderr + пропуск элемента (form-decompile — draft,
|
||
# скрипт не падает на непокрытых конструкциях).
|
||
# =====================================================================
|
||
|
||
# Прочитать дочерний скаляр по xpath (с $ns). Аналог skd Get-Text.
|
||
function Get-Text {
|
||
param($node, [string]$xpath)
|
||
if (-not $node) { return $null }
|
||
if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText }
|
||
$n = $node.SelectSingleNode($xpath, $ns)
|
||
if ($n) { return $n.InnerText } else { return $null }
|
||
}
|
||
|
||
# Мультиязычный текст (LocalStringType) → string (ru) или ordered hash.
|
||
# Алиас на уже существующий Get-LangText (тот же контракт).
|
||
function Get-MLText { param($node) return (Get-LangText $node) }
|
||
|
||
# Презентация: либо мультиязычный LocalStringType, либо плоский xs:string.
|
||
# Get-MLText даёт $null для xs:string (нет v8:item) → откат к InnerText.
|
||
function Get-PresText {
|
||
param($node)
|
||
if (-not $node) { return $null }
|
||
$ml = Get-MLText $node
|
||
if ($null -ne $ml) { return $ml }
|
||
if ($node.InnerText) { return $node.InnerText }
|
||
return $null
|
||
}
|
||
|
||
# Снять namespace-префикс с xsi:type ("dcsset:Foo" → "Foo")
|
||
function Get-LocalXsiType {
|
||
param($node)
|
||
if (-not $node) { return $null }
|
||
$t = $node.GetAttribute("type", $NS_XSI)
|
||
if ($t -match ':(.+)$') { return $matches[1] }
|
||
return $t
|
||
}
|
||
|
||
# Шрифт оформления → объект {@type:Font, ...} (bit-perfect для compile).
|
||
function Get-FontValue {
|
||
param($valNode)
|
||
$f = [ordered]@{ '@type' = 'Font' }
|
||
foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
|
||
$a = $valNode.Attributes[$attrName]
|
||
if ($null -ne $a) { $f[$attrName] = $a.Value }
|
||
}
|
||
return $f
|
||
}
|
||
|
||
# Линия (граница) оформления → объект {@type:Line, width, gap, style}.
|
||
function Get-LineValue {
|
||
param($valNode)
|
||
$obj = [ordered]@{ '@type' = 'Line' }
|
||
$w = $valNode.GetAttribute("width")
|
||
$g = $valNode.GetAttribute("gap")
|
||
if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } }
|
||
if ($g -ne '') { $obj['gap'] = ($g -eq 'true') }
|
||
$styleNode = $valNode.SelectSingleNode("v8ui:style", $ns)
|
||
if ($styleNode) { $obj['style'] = $styleNode.InnerText }
|
||
return $obj
|
||
}
|
||
|
||
# Прочитать <dcscor:value> в JSON-значение: Font/Line/multilang/raw text.
|
||
function Read-AppearanceValueNode {
|
||
param($valNode)
|
||
if (-not $valNode) { return $null }
|
||
$vt = Get-LocalXsiType $valNode
|
||
if ($vt -eq 'LocalStringType') { return (Get-MLText $valNode) }
|
||
if ($vt -eq 'Font') { return (Get-FontValue $valNode) }
|
||
if ($vt -eq 'Line') { return (Get-LineValue $valNode) }
|
||
return $valNode.InnerText
|
||
}
|
||
|
||
# Обратная карта comparisonType → короткий оператор фильтра (зеркало skd).
|
||
$script:filterOpMap = @{
|
||
'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>=';
|
||
'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn';
|
||
'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy';
|
||
'Contains'='contains'; 'NotContains'='notContains';
|
||
'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith';
|
||
'Filled'='filled'; 'NotFilled'='notFilled'
|
||
}
|
||
|
||
# Render filter value node → shorthand-acceptable scalar string
|
||
function Get-FilterValue {
|
||
param($valNode)
|
||
if (-not $valNode) { return '_' }
|
||
$nil = $valNode.GetAttribute("nil", $NS_XSI)
|
||
if ($nil -eq 'true') { return '_' }
|
||
$vType = Get-LocalXsiType $valNode
|
||
if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText }
|
||
if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) }
|
||
$txt = $valNode.InnerText
|
||
if (-not $txt) { return '_' }
|
||
return $txt
|
||
}
|
||
|
||
# Get-FilterValue + xsi:type значения (для valueType, например dcscor:Field).
|
||
function Get-FilterValueWithType {
|
||
param($valNode)
|
||
if (-not $valNode) { return @{ value = '_'; type = $null } }
|
||
$rawType = $valNode.GetAttribute("type", $NS_XSI)
|
||
$nil = $valNode.GetAttribute("nil", $NS_XSI)
|
||
if ($nil -eq 'true') { return @{ value = '_'; type = $null } }
|
||
$vType = Get-LocalXsiType $valNode
|
||
if ($vType -eq 'LocalStringType') {
|
||
return @{ value = (Get-MLText $valNode); type = $rawType }
|
||
}
|
||
$txt = $valNode.InnerText
|
||
if (-not $txt) { return @{ value = '_'; type = $rawType } }
|
||
if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } }
|
||
if ($vType -eq 'decimal') {
|
||
if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } }
|
||
return @{ value = [double]$txt; type = $rawType }
|
||
}
|
||
return @{ value = $txt; type = $rawType }
|
||
}
|
||
|
||
# Convert filter item node → shorthand string или object form (рекурсивно для групп).
|
||
function Build-FilterItem {
|
||
param($itemNode, [string]$loc)
|
||
$xtype = Get-LocalXsiType $itemNode
|
||
if ($xtype -eq 'FilterItemGroup') {
|
||
$gt = Get-Text $itemNode "dcsset:groupType"
|
||
$groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } }
|
||
$items = @()
|
||
foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) {
|
||
$bi = (Build-FilterItem -itemNode $c -loc "$loc/item")
|
||
if ($null -ne $bi) { $items += $bi }
|
||
}
|
||
$gObj = [ordered]@{ group = $groupName; items = $items }
|
||
$gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
|
||
if ($gPresNode) {
|
||
$gPres = Get-MLText $gPresNode
|
||
if (-not $gPres) { $gPres = $gPresNode.InnerText }
|
||
if ($gPres) { $gObj['presentation'] = $gPres }
|
||
}
|
||
$gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
|
||
if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText }
|
||
$gUSID = Get-Text $itemNode "dcsset:userSettingID"
|
||
if ($gUSID) { $gObj['userSettingID'] = 'auto' }
|
||
$gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
|
||
if ($gUSPN) {
|
||
$gUSP = Get-PresText $gUSPN
|
||
if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP }
|
||
}
|
||
return $gObj
|
||
}
|
||
if ($xtype -ne 'FilterItemComparison') {
|
||
[Console]::Error.WriteLine("form-decompile: пропущен фильтр неизвестного типа '$xtype' (path: $loc)")
|
||
return $null
|
||
}
|
||
$leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns)
|
||
$field = if ($leftNode) { $leftNode.InnerText } else { $null }
|
||
$ct = Get-Text $itemNode "dcsset:comparisonType"
|
||
$op = $script:filterOpMap[$ct]
|
||
if (-not $op) { $op = $ct }
|
||
|
||
$rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns))
|
||
$value = $null
|
||
$valueIsArrayFlag = $false
|
||
$valueTypeAttr = $null
|
||
if ($rightNodes.Count -eq 1) {
|
||
$rn = $rightNodes[0]
|
||
if ((Get-LocalXsiType $rn) -eq 'ValueListType') {
|
||
$value = @()
|
||
$valueIsArrayFlag = $true
|
||
} else {
|
||
$vt = Get-FilterValueWithType $rn
|
||
$value = $vt.value
|
||
$autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and `
|
||
("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.')
|
||
if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) {
|
||
$valueTypeAttr = $vt.type
|
||
}
|
||
}
|
||
} elseif ($rightNodes.Count -gt 1) {
|
||
$arr = @()
|
||
$rawTypes = @()
|
||
foreach ($rn in $rightNodes) {
|
||
$arr += (Get-FilterValue $rn)
|
||
$rawTypes += $rn.GetAttribute("type", $NS_XSI)
|
||
}
|
||
$value = $arr
|
||
$valueIsArrayFlag = $true
|
||
$uniqTypes = @($rawTypes | Sort-Object -Unique)
|
||
if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) {
|
||
$autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and `
|
||
($arr.Count -gt 0) -and `
|
||
(@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0)
|
||
if (-not $autoDetectsDTV) {
|
||
$valueTypeAttr = $uniqTypes[0]
|
||
}
|
||
}
|
||
}
|
||
|
||
$use = Get-Text $itemNode "dcsset:use"
|
||
$userId = Get-Text $itemNode "dcsset:userSettingID"
|
||
$vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
|
||
$viewMode = if ($vmNode) { $vmNode.InnerText } else { $null }
|
||
$userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
|
||
$fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
|
||
$fiPres = $null
|
||
if ($fiPresNode) {
|
||
$fiPres = Get-MLText $fiPresNode
|
||
if (-not $fiPres) { $fiPres = $fiPresNode.InnerText }
|
||
}
|
||
|
||
$flags = @()
|
||
if ($use -eq 'false') { $flags += '@off' }
|
||
if ($userId) { $flags += '@user' }
|
||
if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' }
|
||
elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' }
|
||
elseif ($viewMode -eq 'Normal') { $flags += '@normal' }
|
||
|
||
$noValueOps = @('filled','notFilled')
|
||
|
||
if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres) {
|
||
$obj = [ordered]@{ field = $field; op = $op }
|
||
if ($op -notin $noValueOps -and $null -ne $value) {
|
||
if ($valueIsArrayFlag) {
|
||
$arrAsList = New-Object System.Collections.ArrayList
|
||
foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) }
|
||
$obj['value'] = $arrAsList
|
||
} else {
|
||
$obj['value'] = $value
|
||
}
|
||
}
|
||
if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr }
|
||
if ($use -eq 'false') { $obj['use'] = $false }
|
||
if ($userId) { $obj['userSettingID'] = 'auto' }
|
||
if ($fiPres) { $obj['presentation'] = $fiPres }
|
||
if ($viewMode) { $obj['viewMode'] = $viewMode }
|
||
if ($userPresNode) { $obj['userSettingPresentation'] = Get-PresText $userPresNode }
|
||
return $obj
|
||
}
|
||
|
||
$s = $field
|
||
if ($op -in $noValueOps) {
|
||
$s += " $op"
|
||
} else {
|
||
$vDisplay = '_'
|
||
if ($null -ne $value) {
|
||
if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } }
|
||
elseif ("$value" -ne '') { $vDisplay = "$value" }
|
||
}
|
||
$s += " $op $vDisplay"
|
||
}
|
||
if ($flags) { $s += ' ' + ($flags -join ' ') }
|
||
return $s
|
||
}
|
||
|
||
# Рекурсивный хелпер одного элемента selection (для conditionalAppearance).
|
||
function Build-SelectionItem {
|
||
param($item, [string]$loc)
|
||
$xt = Get-LocalXsiType $item
|
||
if (-not $xt) {
|
||
$fName = Get-Text $item "dcsset:field"
|
||
if ($fName) { return $fName }
|
||
$fieldEl = $item.SelectSingleNode("dcsset:field", $ns)
|
||
if ($fieldEl) { return 'Auto' }
|
||
}
|
||
switch ($xt) {
|
||
'SelectedItemAuto' {
|
||
$useV = Get-Text $item "dcsset:use"
|
||
if ($useV -eq 'false') {
|
||
return [ordered]@{ auto = $true; use = $false }
|
||
}
|
||
return 'Auto'
|
||
}
|
||
'SelectedItemField' {
|
||
$fName = Get-Text $item "dcsset:field"
|
||
$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
|
||
$title = Get-MLText $titleNode
|
||
$vmN = $item.SelectSingleNode("dcsset:viewMode", $ns)
|
||
$useV = Get-Text $item "dcsset:use"
|
||
$useFalse = ($useV -eq 'false')
|
||
if ($title -or $vmN -or $useFalse) {
|
||
$obj = [ordered]@{ field = $fName }
|
||
if ($useFalse) { $obj['use'] = $false }
|
||
if ($title) { $obj['title'] = $title }
|
||
if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
|
||
return $obj
|
||
}
|
||
return $fName
|
||
}
|
||
'SelectedItemFolder' {
|
||
$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
|
||
$folderTitle = Get-MLText $titleNode
|
||
$inner = @()
|
||
foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) {
|
||
$bi = (Build-SelectionItem -item $sub -loc "$loc/folder")
|
||
if ($null -ne $bi) { $inner += $bi }
|
||
}
|
||
$entry = [ordered]@{ folder = $folderTitle; items = $inner }
|
||
$folderField = Get-Text $item "dcsset:field"
|
||
if ($folderField) { $entry['field'] = $folderField }
|
||
$plN = $item.SelectSingleNode("dcsset:placement", $ns)
|
||
if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') {
|
||
$entry['placement'] = $plN.InnerText
|
||
}
|
||
return $entry
|
||
}
|
||
default {
|
||
[Console]::Error.WriteLine("form-decompile: пропущен элемент selection неизвестного типа '$xt' (path: $loc)")
|
||
return $null
|
||
}
|
||
}
|
||
}
|
||
|
||
# Build selection items array (для conditionalAppearance).
|
||
function Build-Selection {
|
||
param($selNode, [string]$loc)
|
||
if (-not $selNode) { return @() }
|
||
$out = @()
|
||
foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) {
|
||
$bi = (Build-SelectionItem -item $it -loc $loc)
|
||
if ($null -ne $bi) { $out += $bi }
|
||
}
|
||
return ,$out
|
||
}
|
||
|
||
# Build order items array.
|
||
function Build-Order {
|
||
param($ordNode, [string]$loc)
|
||
if (-not $ordNode) { return @() }
|
||
$out = @()
|
||
foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) {
|
||
$xt = Get-LocalXsiType $it
|
||
switch ($xt) {
|
||
'OrderItemAuto' { $out += 'Auto' }
|
||
'OrderItemField' {
|
||
$fn = Get-Text $it "dcsset:field"
|
||
$ot = Get-Text $it "dcsset:orderType"
|
||
$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
|
||
$useV = Get-Text $it "dcsset:use"
|
||
$useFalse = ($useV -eq 'false')
|
||
if ($vmN -or $useFalse) {
|
||
$obj = [ordered]@{ field = $fn }
|
||
if ($useFalse) { $obj['use'] = $false }
|
||
if ($ot -eq 'Desc') { $obj['direction'] = 'desc' }
|
||
if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
|
||
$out += $obj
|
||
} else {
|
||
if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
|
||
}
|
||
}
|
||
default {
|
||
[Console]::Error.WriteLine("form-decompile: пропущен элемент сортировки неизвестного типа '$xt' (path: $loc)")
|
||
}
|
||
}
|
||
}
|
||
return ,$out
|
||
}
|
||
|
||
# Build appearance dict из <dcsset:appearance> (Line/Font/multilang/nested items).
|
||
function Get-SettingsAppearance {
|
||
param($appNode)
|
||
if (-not $appNode) { return $null }
|
||
$dict = [ordered]@{}
|
||
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
|
||
$pName = Get-Text $it "dcscor:parameter"
|
||
$val = $it.SelectSingleNode("dcscor:value", $ns)
|
||
if (-not $pName -or -not $val) { continue }
|
||
$rawVal = Read-AppearanceValueNode $val
|
||
$useV = Get-Text $it "dcscor:use"
|
||
$nestedItems = [ordered]@{}
|
||
foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
|
||
$subName = Get-Text $sub "dcscor:parameter"
|
||
$subVal = $sub.SelectSingleNode("dcscor:value", $ns)
|
||
if (-not $subName) { continue }
|
||
$subRaw = Read-AppearanceValueNode $subVal
|
||
$subUse = Get-Text $sub "dcscor:use"
|
||
$subEntry = [ordered]@{ value = $subRaw }
|
||
if ($subUse -eq 'false') { $subEntry['use'] = $false }
|
||
$nestedItems[$subName] = $subEntry
|
||
}
|
||
$valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line')
|
||
if ($valIsLine) {
|
||
if ($useV -eq 'false') { $rawVal['use'] = $false }
|
||
if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems }
|
||
$dict[$pName] = $rawVal
|
||
} elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) {
|
||
$wrap = [ordered]@{ value = $rawVal }
|
||
if ($useV -eq 'false') { $wrap['use'] = $false }
|
||
if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems }
|
||
$dict[$pName] = $wrap
|
||
} else {
|
||
$dict[$pName] = $rawVal
|
||
}
|
||
}
|
||
return $dict
|
||
}
|
||
|
||
# Build conditionalAppearance array.
|
||
function Build-ConditionalAppearance {
|
||
param($caNode, [string]$loc)
|
||
if (-not $caNode) { return @() }
|
||
$out = @()
|
||
$i = 0
|
||
foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) {
|
||
$entry = [ordered]@{}
|
||
$scopeNode = $it.SelectSingleNode("dcsset:scope", $ns)
|
||
if ($scopeNode -and $scopeNode.HasChildNodes) {
|
||
[Console]::Error.WriteLine("form-decompile: conditionalAppearance item имеет scope — не воспроизводится в DSL (path: $loc/$i/scope)")
|
||
}
|
||
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
|
||
if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
|
||
$entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection"
|
||
}
|
||
$filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
|
||
if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
|
||
$f = @()
|
||
foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) {
|
||
$bi = (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter")
|
||
if ($null -ne $bi) { $f += $bi }
|
||
}
|
||
$entry['filter'] = $f
|
||
}
|
||
$appNode = $it.SelectSingleNode("dcsset:appearance", $ns)
|
||
$ap = Get-SettingsAppearance $appNode
|
||
if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap }
|
||
$presNode = $it.SelectSingleNode("dcsset:presentation", $ns)
|
||
if ($presNode) {
|
||
$pres = Get-MLText $presNode
|
||
if (-not $pres) { $pres = $presNode.InnerText }
|
||
if ($pres) { $entry['presentation'] = $pres }
|
||
}
|
||
$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
|
||
if ($vmN) { $entry['viewMode'] = $vmN.InnerText }
|
||
$usid = Get-Text $it "dcsset:userSettingID"
|
||
if ($usid) { $entry['userSettingID'] = 'auto' }
|
||
$uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
|
||
if ($uspN) {
|
||
$usp = Get-PresText $uspN
|
||
if ($usp) { $entry['userSettingPresentation'] = $usp }
|
||
}
|
||
$useV = Get-Text $it "dcsset:use"
|
||
if ($useV -eq 'false') { $entry['use'] = $false }
|
||
$useInDontUse = @()
|
||
foreach ($ch in $it.ChildNodes) {
|
||
if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne $NS_DCSSET) { continue }
|
||
if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') {
|
||
$shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1)
|
||
$useInDontUse += $shortName
|
||
}
|
||
}
|
||
if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse }
|
||
$out += $entry
|
||
$i++
|
||
}
|
||
return ,$out
|
||
}
|
||
|
||
# Общие layout-свойства → в $obj (симметрично Emit-Layout компилятора).
|
||
# Вызывается один раз для любого элемента. Height тут — высота элемента (<Height>),
|
||
# в т.ч. у Table; высоту в строках (<HeightInTableRows>) Table ловит отдельно в heightInTableRows.
|
||
function Add-Layout {
|
||
param($obj, $node)
|
||
# Общие свойства элемента (любой тип): default/drag/skip
|
||
if ((Get-Child $node 'DefaultItem') -eq 'true') { $obj['defaultItem'] = $true }
|
||
$soi = Get-Child $node 'SkipOnInput'; if ($null -ne $soi) { $obj['skipOnInput'] = ($soi -eq 'true') }
|
||
# EnableStartDrag/EnableDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet)
|
||
$esd = Get-Child $node 'EnableStartDrag'; if ($null -ne $esd) { $obj['enableStartDrag'] = ($esd -eq 'true') }
|
||
$edr = Get-Child $node 'EnableDrag'; if ($null -ne $edr) { $obj['enableDrag'] = ($edr -eq 'true') }
|
||
$fdm = Get-Child $node 'FileDragMode'; if ($fdm) { $obj['fileDragMode'] = $fdm }
|
||
# AutoMaxWidth: компилятор додумывает false для multiLine-input без явного ключа (multiLineDefault).
|
||
# Захват факт. значения; multiLine-input без тега → autoMaxWidth:true (суппресс эвристики).
|
||
$amwNode = Get-Child $node 'AutoMaxWidth'
|
||
if ($amwNode -eq 'false') { $obj['autoMaxWidth'] = $false }
|
||
elseif ($amwNode -eq 'true') { $obj['autoMaxWidth'] = $true }
|
||
elseif ((Get-Child $node 'MultiLine') -eq 'true') { $obj['autoMaxWidth'] = $true }
|
||
$mw = Get-Child $node 'MaxWidth'; if ($mw) { $obj['maxWidth'] = [int]$mw }
|
||
if ((Get-Child $node 'AutoMaxHeight') -eq 'false') { $obj['autoMaxHeight'] = $false }
|
||
$mh = Get-Child $node 'MaxHeight'; if ($mh) { $obj['maxHeight'] = [int]$mh }
|
||
$w = Get-Child $node 'Width'; if ($w) { $obj['width'] = [int]$w }
|
||
$h = Get-Child $node 'Height'; if ($h) { $obj['height'] = [int]$h }
|
||
# Stretch: захват фактического значения (true И false — платформа эмитит явное)
|
||
$hs = Get-Child $node 'HorizontalStretch'; if ($null -ne $hs) { $obj['horizontalStretch'] = ($hs -eq 'true') }
|
||
$vs = Get-Child $node 'VerticalStretch'; if ($null -ne $vs) { $obj['verticalStretch'] = ($vs -eq 'true') }
|
||
$gha = Get-Child $node 'GroupHorizontalAlign'; if ($gha) { $obj['groupHorizontalAlign'] = $gha }
|
||
$gva = Get-Child $node 'GroupVerticalAlign'; if ($gva) { $obj['groupVerticalAlign'] = $gva }
|
||
$ha = Get-Child $node 'HorizontalAlign'; if ($ha) { $obj['horizontalAlign'] = $ha }
|
||
# Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть»
|
||
foreach ($p in @('ShowInHeader','ShowInFooter','AutoCellHeight')) {
|
||
$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = ($v -eq 'true') }
|
||
}
|
||
$fha = Get-Child $node 'FooterHorizontalAlign'; if ($fha) { $obj['footerHorizontalAlign'] = $fha }
|
||
$hha = Get-Child $node 'HeaderHorizontalAlign'; if ($hha) { $obj['headerHorizontalAlign'] = $hha }
|
||
}
|
||
|
||
# TitleLocation у check/radio (зеркало Emit-TitleLocation):
|
||
# тега нет → "" (дефолт платформы); значение = умный дефолт → опускаем; иначе пишем.
|
||
function Add-TitleLocation {
|
||
param($obj, $node, [string]$smartDefault)
|
||
$tl = Get-Child $node 'TitleLocation'
|
||
if ($null -eq $tl) { $obj['titleLocation'] = '' }
|
||
elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() }
|
||
}
|
||
|
||
# Разобрать <Events> элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика }
|
||
# в порядке документа. Имена обработчиков всегда явные (как у событий формы) —
|
||
# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим.
|
||
function Get-Events {
|
||
param($node, [string]$elName)
|
||
$ev = $node.SelectSingleNode("lf:Events", $ns)
|
||
if (-not $ev) { return $null }
|
||
$events = [ordered]@{}
|
||
foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) {
|
||
$events[$e.GetAttribute("name")] = $e.InnerText
|
||
}
|
||
if ($events.Count -eq 0) { return $null }
|
||
return $events
|
||
}
|
||
|
||
# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use).
|
||
# <TAG><xr:Common/>[<xr:Value name="Role.X"/>…]</TAG> → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }.
|
||
# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует.
|
||
function Decompile-XrFlag {
|
||
param($node, [string]$tag)
|
||
$el = $node.SelectSingleNode("*[local-name()='$tag']")
|
||
if (-not $el) { return $null }
|
||
$commonNode = $el.SelectSingleNode("*[local-name()='Common']")
|
||
$common = ($commonNode -and $commonNode.InnerText -eq 'true')
|
||
$valNodes = @($el.SelectNodes("*[local-name()='Value']"))
|
||
if ($valNodes.Count -eq 0) { return $common }
|
||
$roles = [ordered]@{}
|
||
foreach ($v in $valNodes) {
|
||
$rn = $v.GetAttribute("name")
|
||
if ($rn -match '^Role\.') { $rn = $rn.Substring(5) }
|
||
$roles[$rn] = ($v.InnerText -eq 'true')
|
||
}
|
||
$o = [ordered]@{}
|
||
$o['common'] = $common
|
||
$o['roles'] = $roles
|
||
return $o
|
||
}
|
||
|
||
# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel,
|
||
# каждая — список переопределений команд (платформа эмитит ТОЛЬКО отклонения от авто-расстановки).
|
||
# Элемент: command (verbatim, "0"=пустой) + type (Auto опускаем) + attribute/group(CommandGroup)/index +
|
||
# defaultVisible(bool) + visible(xr-flag bool/{common,roles} — тот же механизм, что userVisible).
|
||
# Голый элемент (только команда, Type=Auto) → строковый shorthand.
|
||
function Decompile-CommandInterface {
|
||
$ciNode = $root.SelectSingleNode("lf:CommandInterface", $ns)
|
||
if (-not $ciNode) { return $null }
|
||
$ci = [ordered]@{}
|
||
foreach ($panel in @(@('CommandBar','commandBar'), @('NavigationPanel','navigationPanel'))) {
|
||
$pn = $ciNode.SelectSingleNode("lf:$($panel[0])", $ns)
|
||
if (-not $pn) { continue }
|
||
$items = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($pn.SelectNodes("lf:Item", $ns))) {
|
||
$o = [ordered]@{}
|
||
$cmd = Get-Child $it 'Command'
|
||
$o['command'] = "$cmd"
|
||
$ty = Get-Child $it 'Type'; if ($ty -and $ty -ne 'Auto') { $o['type'] = $ty }
|
||
$at = Get-Child $it 'Attribute'; if ($at) { $o['attribute'] = $at }
|
||
$cg = Get-Child $it 'CommandGroup'; if ($cg) { $o['group'] = $cg }
|
||
$idx = Get-Child $it 'Index'; if ($null -ne $idx) { $o['index'] = [int]$idx }
|
||
$dv = Get-Child $it 'DefaultVisible'; if ($null -ne $dv) { $o['defaultVisible'] = ($dv -eq 'true') }
|
||
$vis = Decompile-XrFlag $it 'Visible'; if ($null -ne $vis) { $o['visible'] = $vis }
|
||
# Голый элемент (только command) → строка-shorthand; иначе объект
|
||
if ($o.Count -eq 1) { [void]$items.Add("$cmd") } else { [void]$items.Add($o) }
|
||
}
|
||
if ($items.Count -gt 0) { $ci[$panel[1]] = @($items) }
|
||
}
|
||
if ($ci.Count -gt 0) { return $ci }
|
||
return $null
|
||
}
|
||
|
||
# <FunctionalOptions><Item>FunctionalOption.X</Item>…> → массив строк (префикс FunctionalOption. снят; GUID — как есть).
|
||
function Decompile-FunctionalOptions {
|
||
param($node)
|
||
$foNode = $node.SelectSingleNode("lf:FunctionalOptions", $ns)
|
||
if (-not $foNode) { return $null }
|
||
$opts = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($foNode.SelectNodes("lf:Item", $ns))) {
|
||
$t = $it.InnerText.Trim() -replace '^FunctionalOption\.', ''
|
||
[void]$opts.Add($t)
|
||
}
|
||
if ($opts.Count -gt 0) { return ,@($opts) }
|
||
return $null
|
||
}
|
||
|
||
# Колонка реквизита (прямая или внутри AdditionalColumns): name/type/title/functionalOptions.
|
||
function Decompile-AttrColumn {
|
||
param($c)
|
||
$co = [ordered]@{}; $co['name'] = $c.GetAttribute("name")
|
||
$cty = Decompile-Type ($c.SelectSingleNode("lf:Type", $ns)); if ($cty) { $co['type'] = $cty }
|
||
$ctNode = $c.SelectSingleNode("lf:Title", $ns); if ($ctNode) { $t = Get-LangText $ctNode; if ($null -ne $t) { $co['title'] = $t } }
|
||
$cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo }
|
||
return $co
|
||
}
|
||
|
||
# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash
|
||
# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture).
|
||
# Платформа ВСЕГДА эмитит <xr:LoadTransparent> (и true, и false) → дефолт DSL = false.
|
||
# Скаляр (Ref) при loadTransparent=false; объект {src,loadTransparent:true} при true.
|
||
function Get-PictureRef {
|
||
param($node, [string]$picTag)
|
||
$ref = $node.SelectSingleNode("lf:$picTag/xr:Ref", $ns)
|
||
if (-not $ref) { return $null }
|
||
$lt = $node.SelectSingleNode("lf:$picTag/xr:LoadTransparent", $ns)
|
||
if ($lt -and $lt.InnerText -eq 'true') {
|
||
return [ordered]@{ src = $ref.InnerText; loadTransparent = $true }
|
||
}
|
||
return $ref.InnerText
|
||
}
|
||
|
||
# Шрифт <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-LangText $titleNode
|
||
if ($null -ne $t) { $obj['title'] = $t }
|
||
# formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост)
|
||
}
|
||
$ttNode = $node.SelectSingleNode("lf:ToolTip", $ns)
|
||
if ($ttNode) { $tt = Get-LangText $ttNode; if ($null -ne $tt) { $obj['tooltip'] = $tt } }
|
||
$ttr = Get-Child $node 'ToolTipRepresentation'; if ($ttr) { $obj['tooltipRepresentation'] = $ttr }
|
||
# Картинки заголовка/подвала колонки (любой field-тип, эмитятся платформой как column header/footer icon)
|
||
$hp = Get-PictureRef $node 'HeaderPicture'; if ($null -ne $hp) { $obj['headerPicture'] = $hp }
|
||
$fp = Get-PictureRef $node 'FooterPicture'; if ($null -ne $fp) { $obj['footerPicture'] = $fp }
|
||
$ev = Get-Events $node $elName
|
||
if ($ev) { $obj['events'] = $ev }
|
||
}
|
||
|
||
# --- 3. Type decompile (inverse of Emit-Type) ---
|
||
function Decompile-Type {
|
||
param($typeNode)
|
||
if (-not $typeNode) { return $null }
|
||
$parts = New-Object System.Collections.ArrayList
|
||
foreach ($vt in @($typeNode.SelectNodes("v8:Type", $ns))) {
|
||
$raw = $vt.InnerText.Trim()
|
||
$short = $raw
|
||
# break обязателен: иначе общий case ^(v8|v8ui|cfg): перетирает специфичные (напр. v8:ValueListType → ValueList).
|
||
switch -regex ($raw) {
|
||
'^xs:string$' {
|
||
$len = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns)
|
||
if ($len -and [int]$len.InnerText -gt 0) { $short = "string($($len.InnerText))" } else { $short = "string" }
|
||
break
|
||
}
|
||
'^xs:decimal$' {
|
||
$d = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:Digits", $ns)
|
||
$f = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:FractionDigits", $ns)
|
||
$sgn = $typeNode.SelectSingleNode("v8:NumberQualifiers/v8:AllowedSign", $ns)
|
||
$dd = if ($d) { $d.InnerText } else { '0' }
|
||
$ff = if ($f) { $f.InnerText } else { '0' }
|
||
if ($sgn -and $sgn.InnerText -eq 'Nonnegative') { $short = "decimal($dd,$ff,nonneg)" } else { $short = "decimal($dd,$ff)" }
|
||
break
|
||
}
|
||
'^xs:boolean$' { $short = "boolean"; break }
|
||
'^xs:dateTime$' {
|
||
$df = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns)
|
||
$dfv = if ($df) { $df.InnerText } else { 'DateTime' }
|
||
switch ($dfv) { 'Date' { $short = 'date' } 'Time' { $short = 'time' } default { $short = 'dateTime' } }
|
||
break
|
||
}
|
||
'^cfg:(.+)$' { $short = $matches[1]; break }
|
||
'^(v8|v8ui):' {
|
||
# Платформенный тип: friendly-шорткат если есть, иначе оставляем с префиксом
|
||
# (компилятор эмитит verbatim) — чтобы не терять v8:UUID и прочий хвост.
|
||
$rev = @{
|
||
'v8:ValueTable'='ValueTable'; 'v8:ValueTree'='ValueTree'; 'v8:ValueListType'='ValueList'
|
||
'v8:TypeDescription'='TypeDescription'; 'v8:Universal'='Universal'
|
||
'v8:FixedArray'='FixedArray'; 'v8:FixedStructure'='FixedStructure'
|
||
'v8ui:FormattedString'='FormattedString'; 'v8ui:Picture'='Picture'; 'v8ui:Color'='Color'; 'v8ui:Font'='Font'
|
||
}
|
||
if ($rev.ContainsKey($raw)) { $short = $rev[$raw] } else { $short = $raw }
|
||
break
|
||
}
|
||
default { $short = $raw }
|
||
}
|
||
[void]$parts.Add($short)
|
||
}
|
||
# TypeSet (набор типов): определяемый тип / характеристика / «любая ссылка вида».
|
||
# Префикс cfg:/v8: снимаем — обратный роутинг в компиляторе по форме токена.
|
||
foreach ($ts in @($typeNode.SelectNodes("v8:TypeSet", $ns))) {
|
||
$raw = $ts.InnerText.Trim()
|
||
$short = $raw -replace '^(v8ui|v8|cfg):', ''
|
||
[void]$parts.Add($short)
|
||
}
|
||
if ($parts.Count -eq 0) { return $null }
|
||
if ($parts.Count -eq 1) { return $parts[0] }
|
||
return ($parts -join ' | ')
|
||
}
|
||
|
||
# Schema-параметры динамического списка (<Parameter> под <Settings>) — зеркало эмиссии
|
||
# form-compile (Emit-DLParameters). Инверсия контекстных дефолтов: useRestriction=true и
|
||
# title==Title-FromName опускаем (компилятор восстановит). Сущность = DataCompositionSchemaParameter.
|
||
function Build-DLInputParameters {
|
||
param($ipNode)
|
||
$items = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($ipNode.SelectNodes("dcscor:item", $ns))) {
|
||
$io = [ordered]@{}
|
||
$io['parameter'] = Get-Child $it 'parameter'
|
||
$useN = $it.SelectSingleNode("dcscor:use", $ns)
|
||
if ($useN -and $useN.InnerText -eq 'false') { $io['use'] = $false }
|
||
$valN = $it.SelectSingleNode("dcscor:value", $ns)
|
||
if ($valN) {
|
||
$vt = $valN.GetAttribute("type", $NS_XSI)
|
||
if ($vt -match 'ChoiceParameters$') {
|
||
$cps = New-Object System.Collections.ArrayList
|
||
foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) {
|
||
$cpo = [ordered]@{}
|
||
$cpo['name'] = Get-Child $cpi 'choiceParameter'
|
||
$vals = New-Object System.Collections.ArrayList
|
||
foreach ($cv in @($cpi.SelectNodes("dcscor:value", $ns))) {
|
||
[void]$vals.Add((Convert-TypedValue -raw $cv.InnerText -xsiType ($cv.GetAttribute("type", $NS_XSI))))
|
||
}
|
||
$cpo['values'] = @($vals)
|
||
[void]$cps.Add($cpo)
|
||
}
|
||
$io['choiceParameters'] = @($cps)
|
||
} elseif ($vt -match 'ChoiceParameterLinks$') {
|
||
$cpls = New-Object System.Collections.ArrayList
|
||
foreach ($cpi in @($valN.SelectNodes("dcscor:item", $ns))) {
|
||
$cpo = [ordered]@{}
|
||
$cpo['name'] = Get-Child $cpi 'choiceParameter'
|
||
$cpo['value'] = Get-Child $cpi 'value'
|
||
$md = Get-Child $cpi 'mode'; if ($md -and $md -ne 'Auto') { $cpo['mode'] = $md }
|
||
[void]$cpls.Add($cpo)
|
||
}
|
||
$io['choiceParameterLinks'] = @($cpls)
|
||
} else {
|
||
if ($valN.GetAttribute("nil", $NS_XSI) -ne 'true') { $io['value'] = Convert-TypedValue -raw $valN.InnerText -xsiType $vt }
|
||
}
|
||
}
|
||
[void]$items.Add($io)
|
||
}
|
||
return @($items)
|
||
}
|
||
|
||
function Build-DLParameter {
|
||
param($pNode)
|
||
$name = Get-Child $pNode 'name'
|
||
$o = [ordered]@{}
|
||
$o['name'] = $name
|
||
# title — опускаем, если совпадает с авто-выводом из имени (ru-only)
|
||
$titleNode = $pNode.SelectSingleNode("dcssch:title", $ns)
|
||
if ($titleNode) {
|
||
$t = Get-LangText $titleNode
|
||
if ($null -ne $t) {
|
||
$auto = Title-FromName -name $name
|
||
if (-not (($t -is [string]) -and ($t -eq $auto))) { $o['title'] = $t }
|
||
}
|
||
}
|
||
# valueType
|
||
$vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns)
|
||
$typeVal = $null
|
||
if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } }
|
||
# value — опускаем nil (дефолт)
|
||
$vNode = $pNode.SelectSingleNode("dcssch:value", $ns)
|
||
if ($vNode -and ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true')) {
|
||
$o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI))
|
||
}
|
||
# useRestriction — опускаем true (дефолт), фиксируем false
|
||
if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false }
|
||
# expression
|
||
$expr = Get-Child $pNode 'expression'; if ($null -ne $expr -and $expr -ne '') { $o['expression'] = $expr }
|
||
# availableValues
|
||
$avNodes = @($pNode.SelectNodes("dcssch:availableValue", $ns))
|
||
if ($avNodes.Count -gt 0) {
|
||
$avs = New-Object System.Collections.ArrayList
|
||
foreach ($avn in $avNodes) {
|
||
$avo = [ordered]@{}
|
||
$avv = $avn.SelectSingleNode("dcssch:value", $ns)
|
||
if ($avv -and ($avv.GetAttribute("nil", $NS_XSI) -ne 'true')) { $avo['value'] = Convert-TypedValue -raw $avv.InnerText -xsiType ($avv.GetAttribute("type", $NS_XSI)) }
|
||
else { $avo['value'] = $null }
|
||
$avp = $avn.SelectSingleNode("dcssch:presentation", $ns)
|
||
if ($avp) { $pres = Get-LangText $avp; if ($null -ne $pres) { $avo['presentation'] = $pres } }
|
||
[void]$avs.Add($avo)
|
||
}
|
||
$o['availableValues'] = @($avs)
|
||
}
|
||
# valueListAllowed / availableAsField
|
||
if ((Get-Child $pNode 'valueListAllowed') -eq 'true') { $o['valueListAllowed'] = $true }
|
||
if ((Get-Child $pNode 'availableAsField') -eq 'false') { $o['availableAsField'] = $false }
|
||
# inputParameters
|
||
$ipNode = $pNode.SelectSingleNode("dcssch:inputParameters", $ns)
|
||
if ($ipNode) { $ip = Build-DLInputParameters $ipNode; if (@($ip).Count -gt 0) { $o['inputParameters'] = $ip } }
|
||
# denyIncompleteValues / use
|
||
if ((Get-Child $pNode 'denyIncompleteValues') -eq 'true') { $o['denyIncompleteValues'] = $true }
|
||
$use = Get-Child $pNode 'use'; if ($null -ne $use -and $use -ne '') { $o['use'] = $use }
|
||
|
||
# Компактизация: {name} → строка "name"; {name, type} → "name: type"; иначе объект.
|
||
$keys = @($o.Keys)
|
||
if ($keys.Count -eq 1) { return $name }
|
||
if ($keys.Count -eq 2 -and $o.Contains('type') -and ($typeVal -is [string])) { return ("{0}: {1}" -f $name, $typeVal) }
|
||
return $o
|
||
}
|
||
|
||
# --- 4. Element dispatch ---
|
||
$ELEMENT_KEY = @{
|
||
'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; 'ButtonGroup'='buttonGroup'; 'InputField'='input'; 'CheckBoxField'='check';
|
||
'RadioButtonField'='radio'; 'LabelDecoration'='label'; 'LabelField'='labelField';
|
||
'PictureDecoration'='picture'; 'PictureField'='picField'; 'CalendarField'='calendar';
|
||
'Table'='table'; 'Pages'='pages'; 'Page'='page'; 'Button'='button'; 'CommandBar'='cmdBar'; 'Popup'='popup';
|
||
'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl';
|
||
'SpreadSheetDocumentField'='spreadsheet'; 'HTMLDocumentField'='html'; 'TextDocumentField'='textDoc';
|
||
'FormattedDocumentField'='formattedDoc'; 'ProgressBarField'='progressBar'; 'TrackBarField'='trackBar';
|
||
'ChartField'='chart'; 'GraphicalSchemaField'='graphicalSchema'; 'PlannerField'='planner';
|
||
'PeriodField'='periodField'; 'DendrogramField'='dendrogram'; 'GanttChartField'='ganttChart'
|
||
}
|
||
|
||
# Простые скаляры элемента (pass-through, зеркало $script:genericScalars компилятора). kind bool/value.
|
||
$GENERIC_SCALARS = @(
|
||
@{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' }
|
||
@{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' }
|
||
@{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' }
|
||
@{ Tag='PictureSize'; Key='pictureSize'; Kind='value' }
|
||
@{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' }
|
||
@{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' }
|
||
@{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' }
|
||
@{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' }
|
||
@{ Tag='ViewMode'; Key='viewMode'; Kind='value' }
|
||
@{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' }
|
||
@{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' }
|
||
@{ Tag='Mask'; Key='mask'; Kind='value' }
|
||
@{ Tag='CreateButton'; Key='createButton'; Kind='bool' }
|
||
@{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' }
|
||
@{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' }
|
||
# Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through (зеркало компилятора)
|
||
@{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' }
|
||
@{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' }
|
||
@{ Tag='Output'; Key='output'; Kind='value' }
|
||
@{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' }
|
||
@{ Tag='PointerType'; Key='pointerType'; Kind='value' }
|
||
@{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' }
|
||
@{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' }
|
||
@{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' }
|
||
@{ Tag='Protection'; Key='protection'; Kind='bool' }
|
||
@{ Tag='Edit'; Key='edit'; Kind='bool' }
|
||
@{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' }
|
||
@{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' }
|
||
@{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' }
|
||
@{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' }
|
||
@{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' }
|
||
@{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' }
|
||
# Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы
|
||
@{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' }
|
||
@{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' }
|
||
@{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' }
|
||
# Хвост: высота элемента списка (radio) / ширина выпадающего списка (input)
|
||
@{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' }
|
||
@{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' }
|
||
# Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам
|
||
@{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' }
|
||
@{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' }
|
||
@{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' }
|
||
@{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' }
|
||
@{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' }
|
||
@{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' }
|
||
)
|
||
|
||
# Захват generic-скаляров. Специфичная обработка (если ключ уже задан) — побеждает.
|
||
function Add-GenericScalars {
|
||
param($obj, $node)
|
||
foreach ($s in $GENERIC_SCALARS) {
|
||
if ($obj.Contains($s.Key)) { continue }
|
||
$v = Get-Child $node $s.Tag
|
||
if ($null -eq $v) { continue }
|
||
if ($s.Kind -eq 'bool') { $obj[$s.Key] = ($v -eq 'true') } else { $obj[$s.Key] = $v }
|
||
}
|
||
}
|
||
|
||
function Decompile-Children {
|
||
param($parentNode, [string]$childContainer = 'ChildItems')
|
||
$container = $parentNode.SelectSingleNode("lf:$childContainer", $ns)
|
||
if (-not $container) { return $null }
|
||
$list = New-Object System.Collections.ArrayList
|
||
foreach ($child in $container.ChildNodes) {
|
||
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||
if ($COMPANION_TAGS -contains $child.LocalName) { continue }
|
||
$el = Decompile-Element $child
|
||
if ($el) { [void]$list.Add($el) }
|
||
}
|
||
if ($list.Count -eq 0) { return $null }
|
||
return ,@($list)
|
||
}
|
||
|
||
# Инверсия Emit-CompanionPanel: companion-командная-панель (ContextMenu/AutoCommandBar) с контентом
|
||
# → { autofill?, horizontalAlign?, children?[] } либо $null, если companion пустой (self-closing).
|
||
# Дин-список-таблица: компилятор-эвристика додумывает Autofill=false. Выражаем только ОТКЛОНЕНИЕ:
|
||
# autofill=false (нет детей) → совпадает с дефолтом → молчим;
|
||
# голый <AutoCommandBar/> (autofill=true по умолчанию) → маркер { autofill: true } (панель не скрыта).
|
||
function Decompile-CompanionPanel {
|
||
param($node, [string]$tag, [bool]$isDynListTable = $false)
|
||
$p = $node.SelectSingleNode("lf:$tag", $ns)
|
||
if (-not $p) { return $null }
|
||
$autofillRaw = Get-Child $p 'Autofill'
|
||
$halign = Get-Child $p 'HorizontalAlign'
|
||
$kids = Decompile-Children $p
|
||
$hasKids = $kids -and @($kids).Count -gt 0
|
||
if ($isDynListTable -and $tag -eq 'AutoCommandBar' -and -not $hasKids -and -not $halign) {
|
||
if ($autofillRaw -eq 'false') { return $null } # = дефолт эвристики → молчим
|
||
return [ordered]@{ autofill = $true } # голая панель → отклонение
|
||
}
|
||
if (-not $hasKids -and $null -eq $autofillRaw -and -not $halign) { return $null }
|
||
$o = [ordered]@{}
|
||
if ($halign) { $o['horizontalAlign'] = $halign }
|
||
if ($autofillRaw -eq 'false') { $o['autofill'] = $false }
|
||
elseif ($autofillRaw -eq 'true') { $o['autofill'] = $true }
|
||
if ($hasKids) { $o['children'] = $kids }
|
||
return $o
|
||
}
|
||
|
||
# Инверсия Emit-ChoiceList: <ChoiceList><xr:Item>… → [ { value, presentation? } ] либо $null.
|
||
# У RadioButtonField и InputField.
|
||
function Decompile-ChoiceList {
|
||
param($node)
|
||
$cl = $node.SelectSingleNode("lf:ChoiceList", $ns)
|
||
if (-not $cl) { return $null }
|
||
$items = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($cl.SelectNodes("xr:Item", $ns))) {
|
||
$valNode = $it.SelectSingleNode("xr:Value/lf:Value", $ns)
|
||
$presNode = $it.SelectSingleNode("xr:Value/lf:Presentation", $ns)
|
||
$ci = [ordered]@{}
|
||
if ($valNode) {
|
||
$xsiType = $valNode.GetAttribute("type", $NS_XSI)
|
||
$ci['value'] = Convert-TypedValue $valNode.InnerText $xsiType
|
||
}
|
||
# Presentation: непустой → текст/мультиязык; пустой <Presentation/> → "" — суппресс-маркер,
|
||
# подавляет авто-вывод компилятора (иначе компилятор додумает presentation из значения).
|
||
if ($presNode) {
|
||
$p = Get-LangText $presNode
|
||
if ($null -ne $p -and $p -ne '') { $ci['presentation'] = $p } else { $ci['presentation'] = '' }
|
||
}
|
||
[void]$items.Add($ci)
|
||
}
|
||
if ($items.Count -gt 0) { return ,@($items) }
|
||
return $null
|
||
}
|
||
|
||
# Значение Параметра выбора (<lf:Value>): скаляр через Convert-TypedValue, либо
|
||
# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value).
|
||
function Convert-ChoiceParamValue {
|
||
param($valNode)
|
||
$vt = $valNode.GetAttribute("type", $NS_XSI)
|
||
if ($vt -match 'FixedArray$') {
|
||
$arr = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) {
|
||
$inner = $it.SelectSingleNode("lf:Value", $ns)
|
||
if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) }
|
||
}
|
||
return ,@($arr)
|
||
}
|
||
return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt
|
||
}
|
||
|
||
# Инверсия Emit-ChoiceParameters: <ChoiceParameters><app:item name="X"><app:value><Value…> → [{name, value}].
|
||
function Decompile-ChoiceParameters {
|
||
param($node)
|
||
$cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns)
|
||
if (-not $cpn) { return $null }
|
||
$items = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($cpn.SelectNodes("app:item", $ns))) {
|
||
$o = [ordered]@{}
|
||
$o['name'] = $it.GetAttribute("name")
|
||
$valNode = $it.SelectSingleNode("app:value/lf:Value", $ns)
|
||
if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode }
|
||
[void]$items.Add($o)
|
||
}
|
||
if ($items.Count -gt 0) { return ,@($items) }
|
||
return $null
|
||
}
|
||
|
||
# Инверсия Emit-ChoiceParameterLinks: <ChoiceParameterLinks><xr:Link><xr:Name><xr:DataPath><xr:ValueChange> →
|
||
# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит).
|
||
function Decompile-ChoiceParameterLinks {
|
||
param($node)
|
||
$cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns)
|
||
if (-not $cln) { return $null }
|
||
$items = New-Object System.Collections.ArrayList
|
||
foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) {
|
||
$o = [ordered]@{}
|
||
$o['name'] = Get-Text $lk "xr:Name"
|
||
$o['dataPath'] = Get-Text $lk "xr:DataPath"
|
||
$vc = Get-Text $lk "xr:ValueChange"
|
||
if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc }
|
||
[void]$items.Add($o)
|
||
}
|
||
if ($items.Count -gt 0) { return ,@($items) }
|
||
return $null
|
||
}
|
||
|
||
# Инверсия Emit-TypeLink: <TypeLink><xr:DataPath><xr:LinkItem> → {dataPath, linkItem}.
|
||
function Decompile-TypeLink {
|
||
param($node)
|
||
$tn = $node.SelectSingleNode("lf:TypeLink", $ns)
|
||
if (-not $tn) { return $null }
|
||
$o = [ordered]@{}
|
||
$o['dataPath'] = Get-Text $tn "xr:DataPath"
|
||
$li = Get-Text $tn "xr:LinkItem"
|
||
if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li }
|
||
return $o
|
||
}
|
||
|
||
# Захват <Format>/<EditFormat> (LocalStringType) → format/editFormat (строка или {ru,en}).
|
||
function Add-FormatProps {
|
||
param($obj, $node)
|
||
$fmt = $node.SelectSingleNode("lf:Format", $ns); if ($fmt) { $t = Get-LangText $fmt; if ($null -ne $t -and $t -ne '') { $obj['format'] = $t } }
|
||
$efmt = $node.SelectSingleNode("lf:EditFormat", $ns); if ($efmt) { $t = Get-LangText $efmt; if ($null -ne $t -and $t -ne '') { $obj['editFormat'] = $t } }
|
||
}
|
||
|
||
# Ядро дополнения: source + общие свойства (Add-CommonProps) + horizontalLocation.
|
||
# Layout (Add-Layout) добавляется ОТДЕЛЬНО (в Decompile-Element — пост-обработкой, в standalone — явно).
|
||
function Add-AdditionCore {
|
||
param($obj, $node, [string]$elName)
|
||
$src = $node.SelectSingleNode("lf:AdditionSource/lf:Item", $ns); if ($src) { $obj['source'] = $src.InnerText }
|
||
Add-CommonProps $obj $node $elName
|
||
$hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() }
|
||
}
|
||
|
||
# Стандартные дополнения уровня таблицы (прямые дети <Table>): извлечь ТОЛЬКО отклонения в карту
|
||
# { тип: {свойства} }. Имя (=tableName+suffix) и source (=tableName) — дефолтные, опускаем.
|
||
function Decompile-TableAdditions {
|
||
param($tableNode, [string]$tableName)
|
||
$tagToKey = @{ 'SearchStringAddition'='searchString'; 'ViewStatusAddition'='viewStatus'; 'SearchControlAddition'='searchControl' }
|
||
$map = [ordered]@{}
|
||
foreach ($child in $tableNode.ChildNodes) {
|
||
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||
if (-not $tagToKey.ContainsKey($child.LocalName)) { continue }
|
||
$key = $tagToKey[$child.LocalName]
|
||
$nm = $child.GetAttribute("name")
|
||
$o = [ordered]@{}; $o[$key] = $nm
|
||
Add-AdditionCore $o $child $nm
|
||
Add-Layout $o $child
|
||
$o.Remove($key) # имя авто
|
||
if ($o.Contains('source') -and $o['source'] -eq $tableName) { $o.Remove('source') } # source=таблица дефолт
|
||
if ($o.Count -gt 0) { $map[$key] = $o }
|
||
}
|
||
if ($map.Count -gt 0) { return $map }
|
||
return $null
|
||
}
|
||
|
||
# Спец-поля «документ/датчик» — общий скелет поля (имя/path/CommonProps/TitleLocation/editMode).
|
||
# Типоспец. enum/bool скаляры ловит пост-switch Add-GenericScalars; layout/companions — общий хвост.
|
||
function Decompile-SimpleField {
|
||
param($obj, $node, [string]$name, [string]$key)
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
|
||
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
|
||
}
|
||
|
||
# Числовые скаляры датчиков (ProgressBar/TrackBar) — без xsi:type (≠ типизированных InputField).
|
||
function Add-GaugeScalars {
|
||
param($obj, $node, $tags)
|
||
foreach ($p in $tags) {
|
||
$v = Get-Child $node $p
|
||
if ($null -eq $v) { continue }
|
||
$key = $p.Substring(0,1).ToLower() + $p.Substring(1)
|
||
if ($v -match '^-?\d+$') { $obj[$key] = [int]$v } else { $obj[$key] = $v }
|
||
}
|
||
}
|
||
|
||
function Decompile-Element {
|
||
param($node)
|
||
$tag = $node.LocalName
|
||
if (-not $ELEMENT_KEY.ContainsKey($tag)) {
|
||
Fail-Ring3 -kind "элемент <$tag>" -loc "ChildItems/$tag"
|
||
}
|
||
$key = $ELEMENT_KEY[$tag]
|
||
$name = $node.GetAttribute("name")
|
||
$obj = [ordered]@{}
|
||
|
||
switch ($tag) {
|
||
'UsualGroup' {
|
||
# group = направление (<Group>); behavior = <Behavior> (Авто = нет тега → ключ опускаем).
|
||
# group = направление (<Group>). Нет тега → '' (тип-маркер сохраняется, направление
|
||
# не эмитим). Платформа явно пишет Vertical в большинстве случаев, поэтому '' ≠ 'vertical'
|
||
# — иначе компилятор додумает <Group>Vertical</Group> там, где его нет.
|
||
$g = Get-Child $node 'Group'
|
||
$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical' }
|
||
if ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] } else { $obj[$key] = '' }
|
||
$behavior = Get-Child $node 'Behavior'
|
||
if ($behavior) {
|
||
$bmap = @{ 'Usual'='usual'; 'Collapsible'='collapsible'; 'PopUp'='popup' }
|
||
if ($bmap.ContainsKey($behavior)) { $obj['behavior'] = $bmap[$behavior] } else { $obj['behavior'] = $behavior }
|
||
}
|
||
$obj['name'] = $name
|
||
Add-CommonProps $obj $node $name
|
||
$rep = Get-Child $node 'Representation'
|
||
if ($rep) { $repmap=@{'None'='none';'NormalSeparation'='normal';'WeakSeparation'='weak';'StrongSeparation'='strong'}; if ($repmap.ContainsKey($rep)) { $obj['representation']=$repmap[$rep] } else { $obj['representation']=$rep } }
|
||
if ((Get-Child $node 'ShowTitle') -eq 'false') { $obj['showTitle'] = $false }
|
||
$crt = $node.SelectSingleNode("lf:CollapsedRepresentationTitle", $ns); if ($crt) { $ct = Get-LangText $crt; if ($null -ne $ct -and $ct -ne '') { $obj['collapsedTitle'] = $ct } }
|
||
if ((Get-Child $node 'United') -eq 'false') { $obj['united'] = $false }
|
||
if ((Get-Child $node 'Collapsed') -eq 'true') { $obj['collapsed'] = $true }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'ColumnGroup' {
|
||
# columnGroup = направление (<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
|
||
if ((Get-Child $node 'ShowTitle') -eq 'false') { $obj['showTitle'] = $false }
|
||
$sih = Get-Child $node 'ShowInHeader'; if ($null -ne $sih) { $obj['showInHeader'] = (To-Bool $sih) }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'InputField' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
if ((Get-Child $node 'MultiLine') -eq 'true') { $obj['multiLine'] = $true }
|
||
if ((Get-Child $node 'PasswordMode') -eq 'true') { $obj['passwordMode'] = $true }
|
||
$mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') }
|
||
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
|
||
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
|
||
$ih = $node.SelectSingleNode("lf:InputHint", $ns); if ($ih) { $t = Get-LangText $ih; if ($t) { $obj['inputHint'] = $t } }
|
||
$woe = $node.SelectSingleNode("lf:WarningOnEdit", $ns); if ($woe) { $t = Get-LangText $woe; if ($null -ne $t) { $obj['warningOnEdit'] = $t } }
|
||
$ftxt = $node.SelectSingleNode("lf:FooterText", $ns); if ($ftxt) { $t = Get-LangText $ftxt; if ($null -ne $t) { $obj['footerText'] = $t } }
|
||
foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton','ChoiceListButton')) {
|
||
$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) }
|
||
}
|
||
# InputField-специфичные bool-скаляры (захват «как есть»)
|
||
foreach ($p in @('Wrap','OpenButton','ListChoiceMode','ExtendedEditMultipleValues','ChooseType','QuickChoice','AutoChoiceIncomplete')) {
|
||
$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) }
|
||
}
|
||
# InputField-специфичные value-скаляры (захват «как есть»)
|
||
foreach ($p in @('ChoiceForm','ChoiceHistoryOnInput','ChoiceFoldersAndItems','FooterDataPath')) {
|
||
$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = $v }
|
||
}
|
||
# MinValue/MaxValue — типизированное (<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 }
|
||
}
|
||
}
|
||
$cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr }
|
||
$cbp = Get-PictureRef $node 'ChoiceButtonPicture'; if ($null -ne $cbp) { $obj['choiceButtonPicture'] = $cbp }
|
||
if ((Get-Child $node 'TextEdit') -eq 'false') { $obj['textEdit'] = $false }
|
||
$cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl }
|
||
Add-FormatProps $obj $node
|
||
# Параметры выбора / Связи параметров выбора / Связь по типу
|
||
$cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp }
|
||
$cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl }
|
||
$tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk }
|
||
}
|
||
'CheckBoxField' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
|
||
# CheckBoxType: Auto = умный дефолт → опустить; нет тега → ""; иначе значение
|
||
$cbt = Get-Child $node 'CheckBoxType'
|
||
if ($null -eq $cbt) { $obj['checkBoxType'] = '' }
|
||
elseif ($cbt -ne 'Auto') { $obj['checkBoxType'] = $cbt.Substring(0,1).ToLower() + $cbt.Substring(1) }
|
||
Add-TitleLocation $obj $node 'Right'
|
||
Add-FormatProps $obj $node
|
||
}
|
||
'RadioButtonField' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
Add-TitleLocation $obj $node 'None'
|
||
$rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt }
|
||
$cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc }
|
||
$cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl }
|
||
}
|
||
'LabelDecoration' {
|
||
$obj[$key] = $name
|
||
Add-CommonProps $obj $node $name
|
||
if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
|
||
# title декорации — единая ML-text форма с авто-детектом formatted (как extendedTooltip)
|
||
$tiNode = $node.SelectSingleNode("lf:Title", $ns)
|
||
if ($tiNode) { $tv = Get-MLFormattedValue $tiNode; if ($null -ne $tv) { $obj['title'] = $tv } }
|
||
}
|
||
'LabelField' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
|
||
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
|
||
# LabelField: тег <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') }
|
||
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 } }
|
||
$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns)
|
||
$abs = $node.SelectSingleNode("lf:Picture/xr:Abs", $ns)
|
||
if ($ref) { $obj['src'] = $ref.InnerText } elseif ($abs) { $obj['src'] = "abs:$($abs.InnerText)" } # встроенная картинка → префикс abs:
|
||
$lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'true') { $obj['loadTransparent'] = $true }
|
||
# Прозрачный пиксель картинки (<xr:TransparentPixel x y/>) — координаты фона прозрачности
|
||
$tpx = $node.SelectSingleNode("lf:Picture/xr:TransparentPixel", $ns)
|
||
if ($tpx) { $obj['transparentPixel'] = [ordered]@{ x = [int]$tpx.GetAttribute('x'); y = [int]$tpx.GetAttribute('y') } }
|
||
if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
|
||
}
|
||
'PictureField' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$em = Get-Child $node 'EditMode'; if ($em) { $obj['editMode'] = $em }
|
||
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
|
||
if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
|
||
$sct = Get-Child $node 'Shortcut'; if ($sct) { $obj['shortcut'] = $sct }
|
||
$vp = Get-PictureRef $node 'ValuesPicture'; if ($null -ne $vp) { $obj['valuesPicture'] = $vp }
|
||
$npt = $node.SelectSingleNode("lf:NonselectedPictureText", $ns); if ($npt) { $t = Get-LangText $npt; if ($null -ne $t) { $obj['nonselectedPictureText'] = $t } }
|
||
}
|
||
'CalendarField' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
|
||
$sm = Get-Child $node 'SelectionMode'; if ($sm) { $obj['selectionMode'] = $sm }
|
||
$scd = Get-Child $node 'ShowCurrentDate'; if ($null -ne $scd) { $obj['showCurrentDate'] = ($scd -eq 'true') }
|
||
$wim = Get-Child $node 'WidthInMonths'; if ($null -ne $wim) { $obj['widthInMonths'] = [int]$wim }
|
||
$him = Get-Child $node 'HeightInMonths'; if ($null -ne $him) { $obj['heightInMonths'] = [int]$him }
|
||
$smp = Get-Child $node 'ShowMonthsPanel'; if ($null -ne $smp) { $obj['showMonthsPanel'] = ($smp -eq 'true') }
|
||
}
|
||
'Table' {
|
||
$obj[$key] = $name
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$tl = Get-Child $node 'TitleLocation'; if ($tl) { $obj['titleLocation'] = $tl.ToLower() }
|
||
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
|
||
$crs = Get-Child $node 'ChangeRowSet'; if ($null -ne $crs) { $obj['changeRowSet'] = ($crs -eq 'true') }
|
||
$cro = Get-Child $node 'ChangeRowOrder'; if ($null -ne $cro) { $obj['changeRowOrder'] = ($cro -eq 'true') }
|
||
if ((Get-Child $node 'AutoInsertNewRow') -eq 'true') { $obj['autoInsertNewRow'] = $true }
|
||
# enableDrag — теперь общий (Add-Layout, фактическое значение)
|
||
if ($node.SelectSingleNode("lf:RowFilter", $ns)) { $obj['rowFilter'] = $null }
|
||
if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false }
|
||
if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true }
|
||
# Высота в строках — отдельный ключ heightInTableRows (≠ height = <Height>, его ловит Add-Layout)
|
||
$htr = Get-Child $node 'HeightInTableRows'; if ($htr) { $obj['heightInTableRows'] = [int]$htr }
|
||
# CommandBarLocation: для дин-список-таблицы компилятор авто-инжектит "None" → инвертируем
|
||
# (нет тега → суппресс-маркер ""; "None" → опускаем = авто-дефолт; иначе → захват).
|
||
$cbl = Get-Child $node 'CommandBarLocation'
|
||
if (Has-Child $node 'UpdateOnDataChange') {
|
||
if ($null -eq $cbl) { $obj['commandBarLocation'] = '' }
|
||
elseif ($cbl -ne 'None') { $obj['commandBarLocation'] = $cbl }
|
||
} elseif ($cbl) { $obj['commandBarLocation'] = $cbl }
|
||
$ssl = Get-Child $node 'SearchStringLocation'; if ($ssl) { $obj['searchStringLocation'] = $ssl }
|
||
$vsl = Get-Child $node 'ViewStatusLocation'; if ($vsl) { $obj['viewStatusLocation'] = $vsl }
|
||
$scl = Get-Child $node 'SearchControlLocation'; if ($scl) { $obj['searchControlLocation'] = $scl }
|
||
# --- Общие свойства таблицы (любой тип таблицы, не только динсписок) ---
|
||
if ((Get-Child $node 'ChoiceMode') -eq 'true') { $obj['choiceMode'] = $true }
|
||
$selm = Get-Child $node 'SelectionMode'; if ($selm) { $obj['selectionMode'] = $selm }
|
||
$rsm = Get-Child $node 'RowSelectionMode'; if ($rsm) { $obj['rowSelectionMode'] = $rsm }
|
||
if ((Get-Child $node 'VerticalLines') -eq 'false') { $obj['verticalLines'] = $false }
|
||
if ((Get-Child $node 'HorizontalLines') -eq 'false') { $obj['horizontalLines'] = $false }
|
||
if ((Get-Child $node 'UseAlternationRowColor') -eq 'true') { $obj['useAlternationRowColor'] = $true }
|
||
# Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill).
|
||
$taf = Get-Child $node 'Autofill'; if ($null -ne $taf) { $obj['autofill'] = ($taf -eq 'true') }
|
||
if ((Get-Child $node 'MultipleChoice') -eq 'true') { $obj['multipleChoice'] = $true }
|
||
$soin = Get-Child $node 'SearchOnInput'; if ($soin) { $obj['searchOnInput'] = $soin }
|
||
$mi = Get-Child $node 'AutoMarkIncomplete'; if ($null -ne $mi) { $obj['markIncomplete'] = ($mi -eq 'true') }
|
||
$itv = Get-Child $node 'InitialTreeView'; if ($itv) { $obj['initialTreeView'] = $itv }
|
||
# RowsPicture: src (xr:Ref) + LoadTransparent (дефолт false). При true → объектная форма.
|
||
$rpRef = $node.SelectSingleNode("lf:RowsPicture/xr:Ref", $ns)
|
||
if ($rpRef) {
|
||
$rpLt = $node.SelectSingleNode("lf:RowsPicture/xr:LoadTransparent", $ns)
|
||
if ($rpLt -and $rpLt.InnerText -eq 'true') { $obj['rowsPicture'] = [ordered]@{ src = $rpRef.InnerText; loadTransparent = $true } }
|
||
else { $obj['rowsPicture'] = $rpRef.InnerText }
|
||
}
|
||
$rpdp = Get-Child $node 'RowPictureDataPath'
|
||
# --- Блок дин-список-таблицы (признак: дочерний <UpdateOnDataChange>) ---
|
||
if (Has-Child $node 'UpdateOnDataChange') {
|
||
if ((Get-Child $node 'AutoRefresh') -eq 'true') { $obj['autoRefresh'] = $true }
|
||
$arp = Get-Child $node 'AutoRefreshPeriod'; if ($arp -and $arp -ne '60') { $obj['autoRefreshPeriod'] = [int]$arp }
|
||
$cfi = Get-Child $node 'ChoiceFoldersAndItems'; if ($cfi -and $cfi -ne 'Items') { $obj['choiceFoldersAndItems'] = $cfi }
|
||
if ((Get-Child $node 'RestoreCurrentRow') -eq 'true') { $obj['restoreCurrentRow'] = $true }
|
||
if ((Get-Child $node 'ShowRoot') -eq 'false') { $obj['showRoot'] = $false }
|
||
if ((Get-Child $node 'AllowRootChoice') -eq 'true') { $obj['allowRootChoice'] = $true }
|
||
$uodc = Get-Child $node 'UpdateOnDataChange'; if ($uodc -and $uodc -ne 'Auto') { $obj['updateOnDataChange'] = $uodc }
|
||
if ((Get-Child $node 'AllowGettingCurrentRowURL') -eq 'false') { $obj['allowGettingCurrentRowURL'] = $false }
|
||
# RowPictureDataPath: инверсия умного дефолта <Список>.DefaultPicture
|
||
if ($null -eq $rpdp) { $obj['rowPictureDataPath'] = '' }
|
||
elseif ($rpdp -ne "$($obj['path']).DefaultPicture") { $obj['rowPictureDataPath'] = $rpdp }
|
||
$usg = Get-Child $node 'UserSettingsGroup'; if ($usg) { $obj['userSettingsGroup'] = $usg }
|
||
} elseif ($rpdp) { $obj['rowPictureDataPath'] = $rpdp }
|
||
$csNode = $node.SelectSingleNode("lf:CommandSet", $ns)
|
||
if ($csNode) {
|
||
$exc = New-Object System.Collections.ArrayList
|
||
foreach ($ec in @($csNode.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$exc.Add($ec.InnerText) }
|
||
if ($exc.Count -gt 0) { $obj['excludedCommands'] = @($exc) }
|
||
}
|
||
$cols = Decompile-Children $node
|
||
if ($cols) { $obj['columns'] = $cols }
|
||
# Стандартные дополнения уровня таблицы (прямые дети) → карта отклонений additions
|
||
$addMap = Decompile-TableAdditions $node $name
|
||
if ($addMap) { $obj['additions'] = $addMap }
|
||
}
|
||
'Pages' {
|
||
$obj[$key] = $name
|
||
Add-CommonProps $obj $node $name
|
||
$pr = Get-Child $node 'PagesRepresentation'; if ($pr) { $obj['pagesRepresentation'] = $pr }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'Page' {
|
||
$obj[$key] = $name
|
||
Add-CommonProps $obj $node $name
|
||
$g = Get-Child $node 'Group'
|
||
$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical' }
|
||
if ($g -and $gmap.ContainsKey($g)) { $obj['group'] = $gmap[$g] }
|
||
if ((Get-Child $node 'ShowTitle') -eq 'false') { $obj['showTitle'] = $false }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'Button' {
|
||
$obj[$key] = $name
|
||
$cmd = Get-Child $node 'CommandName'
|
||
if ($cmd) {
|
||
if ($cmd -match '^Form\.Command\.(.+)$') { $obj['command'] = $matches[1] }
|
||
elseif ($cmd -match '^Form\.StandardCommand\.(.+)$') { $obj['stdCommand'] = $matches[1] }
|
||
elseif ($cmd -match '^Form\.Item\.(.+)\.StandardCommand\.(.+)$') { $obj['stdCommand'] = "$($matches[1]).$($matches[2])" }
|
||
else { $obj['commandName'] = $cmd }
|
||
}
|
||
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
|
||
Add-CommonProps $obj $node $name
|
||
$type = Get-Child $node 'Type'
|
||
if ($type) { $tmap=@{'CommandBarButton'='commandBar';'UsualButton'='usual';'Hyperlink'='hyperlink';'CommandBarHyperlink'='hyperlink'}; if ($tmap.ContainsKey($type)) { $obj['type']=$tmap[$type] } else { $obj['type']=$type } }
|
||
if ((Get-Child $node 'DefaultButton') -eq 'true') { $obj['defaultButton'] = $true }
|
||
if ((Get-Child $node 'Check') -eq 'true') { $obj['checked'] = $true }
|
||
$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['picture'] = $ref.InnerText }
|
||
# Дефолт у picture кнопки/попапа = true → фиксируем только отклонение false (true опускаем)
|
||
$lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'false') { $obj['loadTransparent'] = $false }
|
||
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
|
||
$lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic }
|
||
}
|
||
'ButtonGroup' {
|
||
$obj[$key] = $name
|
||
Add-CommonProps $obj $node $name
|
||
$cs = Get-Child $node 'CommandSource'; if ($cs) { $obj['commandSource'] = $cs }
|
||
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'CommandBar' {
|
||
$obj[$key] = $name
|
||
Add-CommonProps $obj $node $name
|
||
$cs = Get-Child $node 'CommandSource'; if ($cs) { $obj['commandSource'] = $cs }
|
||
$hl = Get-Child $node 'HorizontalLocation'; if ($hl) { $obj['horizontalLocation'] = $hl.ToLower() }
|
||
if ((Get-Child $node 'Autofill') -eq 'true') { $obj['autofill'] = $true }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'Popup' {
|
||
$obj[$key] = $name
|
||
Add-CommonProps $obj $node $name
|
||
$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['picture'] = $ref.InnerText }
|
||
# Дефолт у picture кнопки/попапа = true → фиксируем только отклонение false (true опускаем)
|
||
$lt = $node.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'false') { $obj['loadTransparent'] = $false }
|
||
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
|
||
$kids = Decompile-Children $node
|
||
if ($kids) { $obj['children'] = $kids }
|
||
}
|
||
'SearchStringAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name }
|
||
'ViewStatusAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name }
|
||
'SearchControlAddition' { $obj[$key] = $name; Add-AdditionCore $obj $node $name }
|
||
'SpreadSheetDocumentField' { Decompile-SimpleField $obj $node $name $key }
|
||
'HTMLDocumentField' { Decompile-SimpleField $obj $node $name $key }
|
||
'TextDocumentField' { Decompile-SimpleField $obj $node $name $key }
|
||
'FormattedDocumentField' { Decompile-SimpleField $obj $node $name $key }
|
||
'ProgressBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue') }
|
||
'TrackBarField' { Decompile-SimpleField $obj $node $name $key; Add-GaugeScalars $obj $node @('MinValue','MaxValue','LargeStep','MarkingStep','Step') }
|
||
'ChartField' { Decompile-SimpleField $obj $node $name $key }
|
||
'GraphicalSchemaField' { Decompile-SimpleField $obj $node $name $key }
|
||
'PlannerField' { Decompile-SimpleField $obj $node $name $key }
|
||
'PeriodField' { Decompile-SimpleField $obj $node $name $key }
|
||
'DendrogramField' { Decompile-SimpleField $obj $node $name $key }
|
||
'GanttChartField' {
|
||
Decompile-SimpleField $obj $node $name $key
|
||
# Вложенная <Table> (полноценная таблица) — переиспользуем общий Decompile-Element.
|
||
# Ключ ganttTable (не 'table' — во избежание коллизии с тип-ключом таблицы в диспетчере).
|
||
$tblNode = $node.SelectSingleNode("lf:Table", $ns)
|
||
if ($tblNode) { $obj['ganttTable'] = Decompile-Element $tblNode }
|
||
}
|
||
}
|
||
# DisplayImportance — атрибут открывающего тега (адаптивная важность отображения), захват «как есть».
|
||
$di = $node.GetAttribute("DisplayImportance"); if ($di) { $obj['displayImportance'] = $di }
|
||
# title: "" — подавление авто-вывода: для типов, где компилятор вывел бы
|
||
# заголовок из имени, а в оригинале <Title> отсутствует.
|
||
if (-not $obj.Contains('title')) {
|
||
$autoTitle = $false
|
||
if ($tag -in @('LabelDecoration','Page','Popup')) { $autoTitle = $true }
|
||
elseif ($tag -eq 'Button') { $autoTitle = -not ($obj.Contains('command') -or $obj.Contains('commandName') -or $obj.Contains('stdCommand')) }
|
||
elseif ($tag -in @('InputField','CheckBoxField','RadioButtonField','LabelField','Table','CalendarField')) { $autoTitle = -not $obj.Contains('path') }
|
||
if ($autoTitle) { $obj['title'] = '' }
|
||
}
|
||
Add-Layout $obj $node
|
||
Add-GenericScalars $obj $node
|
||
# extendedTooltip: companion <ExtendedTooltip> (это LabelDecoration). Текст-форма (только <Title>) →
|
||
# строка/{text,formatted}. Own-content (layout/оформление/флаги/hyperlink) → объект { text?, … }.
|
||
# Events компаньона пока НЕ захватываем (хвост — требует имя-обработчик).
|
||
$etNode = $node.SelectSingleNode("lf:ExtendedTooltip", $ns)
|
||
if ($etNode) {
|
||
$etTitle = $etNode.SelectSingleNode("lf:Title", $ns)
|
||
$textVal = if ($etTitle) { Get-MLFormattedValue $etTitle } else { $null }
|
||
$etObj = [ordered]@{}
|
||
Add-Layout $etObj $etNode
|
||
Add-GenericScalars $etObj $etNode
|
||
Add-Appearance $etObj $etNode
|
||
if ((Get-Child $etNode 'Visible') -eq 'false') { $etObj['hidden'] = $true }
|
||
if ((Get-Child $etNode 'Enabled') -eq 'false') { $etObj['disabled'] = $true }
|
||
if ((Get-Child $etNode 'Hyperlink') -eq 'true') { $etObj['hyperlink'] = $true }
|
||
if ($etObj.Count -gt 0) {
|
||
if ($null -ne $textVal) {
|
||
if ($textVal -is [System.Collections.IDictionary] -and $textVal.Contains('text')) { $etObj['text'] = $textVal['text']; if ($textVal['formatted']) { $etObj['formatted'] = $true } }
|
||
else { $etObj['text'] = $textVal }
|
||
# formatted ЯВНО из атрибута Title — компилятор не re-детектит markup на мультиязычном тексте
|
||
if (-not $etObj.Contains('formatted') -and $etTitle -and $etTitle.GetAttribute('formatted') -eq 'true') { $etObj['formatted'] = $true }
|
||
}
|
||
$obj['extendedTooltip'] = $etObj
|
||
} elseif ($null -ne $textVal) {
|
||
$obj['extendedTooltip'] = $textVal
|
||
}
|
||
}
|
||
# companion-панели с контентом: AutoCommandBar → commandBar, ContextMenu → contextMenu (любой элемент)
|
||
$isDynListTable = ($tag -eq 'Table') -and (Has-Child $node 'UpdateOnDataChange')
|
||
$cb = Decompile-CompanionPanel $node 'AutoCommandBar' $isDynListTable
|
||
if ($null -ne $cb) { $obj['commandBar'] = $cb }
|
||
$cm = Decompile-CompanionPanel $node 'ContextMenu'
|
||
if ($null -ne $cm) { $obj['contextMenu'] = $cm }
|
||
return $obj
|
||
}
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner на реквизите.
|
||
# Полный захват каждого поля (раундтрип бит-в-бит); зеркало Emit-PlannerSettings.
|
||
function PLD-Bool { param($v) if ($null -eq $v) { return $null } return ($v -eq 'true') }
|
||
function PLD-Int { param($v) if ($null -eq $v) { return $null } return [int]$v }
|
||
# <pl:value> → текст (тип xr:DesignTimeRef/xs:string выводится компилятором из вида значения); nil → $null.
|
||
function Get-PlannerValue {
|
||
param($node)
|
||
if (-not $node) { return $null }
|
||
if ($node.GetAttribute('nil', $NS_XSI) -eq 'true') { return $null }
|
||
if ($node.InnerText) { return $node.InnerText } else { return $null }
|
||
}
|
||
function Build-PlannerFont {
|
||
param($node)
|
||
if (-not $node) { return $null }
|
||
$o = [ordered]@{}
|
||
foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
|
||
$av = $node.GetAttribute($a); if ($av -ne '') { $o[$a] = $av }
|
||
}
|
||
if ($o.Count -eq 0) { return $null }
|
||
return $o
|
||
}
|
||
function Build-PlannerBorder {
|
||
param($node)
|
||
if (-not $node) { return $null }
|
||
$o = [ordered]@{}
|
||
$w = $node.GetAttribute('width'); if ($w -ne '') { $o['width'] = [int]$w }
|
||
$st = $node.SelectSingleNode("*[local-name()='style']"); if ($st) { $o['style'] = $st.InnerText }
|
||
return $o
|
||
}
|
||
function Build-PlannerItem {
|
||
param($itn)
|
||
$o = [ordered]@{}
|
||
$valNode = $itn.SelectSingleNode("*[local-name()='value']")
|
||
if ($valNode -and $valNode.GetAttribute('nil', $NS_XSI) -ne 'true' -and $valNode.InnerText) { $o['value'] = $valNode.InnerText }
|
||
$o['text'] = (Get-Child $itn 'text')
|
||
$tt = Get-Child $itn 'tooltip'; if ($tt) { $o['tooltip'] = $tt }
|
||
$o['begin'] = (Get-Child $itn 'begin')
|
||
$o['end'] = (Get-Child $itn 'end')
|
||
$o['borderColor'] = (Get-Child $itn 'borderColor')
|
||
$o['backColor'] = (Get-Child $itn 'backColor')
|
||
$o['textColor'] = (Get-Child $itn 'textColor')
|
||
$fnt = Build-PlannerFont ($itn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt }
|
||
$o['replacementDate'] = (Get-Child $itn 'replacementDate')
|
||
$o['deleted'] = (PLD-Bool (Get-Child $itn 'deleted'))
|
||
$o['id'] = (Get-Child $itn 'id')
|
||
$o['textFormatted'] = (PLD-Bool (Get-Child $itn 'textFormatted'))
|
||
$brd = Build-PlannerBorder ($itn.SelectSingleNode("*[local-name()='border']")); if ($brd) { $o['border'] = $brd }
|
||
$o['editMode'] = (Get-Child $itn 'editMode')
|
||
return $o
|
||
}
|
||
function Build-PlannerDimElement {
|
||
param($eln)
|
||
$o = [ordered]@{}
|
||
$v = Get-PlannerValue ($eln.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v }
|
||
$o['text'] = (Get-Child $eln 'text')
|
||
$o['borderColor'] = (Get-Child $eln 'borderColor')
|
||
$o['backColor'] = (Get-Child $eln 'backColor')
|
||
$o['textColor'] = (Get-Child $eln 'textColor')
|
||
$fnt = Build-PlannerFont ($eln.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt }
|
||
$subs = New-Object System.Collections.ArrayList
|
||
foreach ($s in @($eln.SelectNodes("*[local-name()='item']"))) { [void]$subs.Add((Build-PlannerDimElement $s)) }
|
||
if ($subs.Count -gt 0) { $o['elements'] = @($subs) }
|
||
$sos = Get-Child $eln 'showOnlySubordinatesAreas'; if ($null -ne $sos) { $o['showOnlySubordinatesAreas'] = ($sos -eq 'true') }
|
||
$o['textFormatted'] = (PLD-Bool (Get-Child $eln 'textFormatted'))
|
||
return $o
|
||
}
|
||
function Build-PlannerDimension {
|
||
param($dn)
|
||
$o = [ordered]@{}
|
||
$v = Get-PlannerValue ($dn.SelectSingleNode("*[local-name()='value']")); if ($null -ne $v) { $o['value'] = $v }
|
||
$o['text'] = (Get-Child $dn 'text')
|
||
$o['borderColor'] = (Get-Child $dn 'borderColor')
|
||
$o['backColor'] = (Get-Child $dn 'backColor')
|
||
$o['textColor'] = (Get-Child $dn 'textColor')
|
||
$fnt = Build-PlannerFont ($dn.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $o['font'] = $fnt }
|
||
$els = New-Object System.Collections.ArrayList
|
||
foreach ($e in @($dn.SelectNodes("*[local-name()='item']"))) { [void]$els.Add((Build-PlannerDimElement $e)) }
|
||
if ($els.Count -gt 0) { $o['elements'] = @($els) }
|
||
$o['textFormatted'] = (PLD-Bool (Get-Child $dn 'textFormatted'))
|
||
return $o
|
||
}
|
||
function Build-PlannerLevel {
|
||
param($lvn)
|
||
$o = [ordered]@{}
|
||
$o['measure'] = (Get-Child $lvn 'measure')
|
||
$o['interval'] = (PLD-Int (Get-Child $lvn 'interval'))
|
||
$o['show'] = (PLD-Bool (Get-Child $lvn 'show'))
|
||
$lineNode = $lvn.SelectSingleNode("*[local-name()='line']")
|
||
if ($lineNode) {
|
||
$ln = [ordered]@{}
|
||
$w = $lineNode.GetAttribute('width'); if ($w -ne '') { $ln['width'] = [int]$w }
|
||
$g = $lineNode.GetAttribute('gap'); if ($g -ne '') { $ln['gap'] = ($g -eq 'true') }
|
||
$st = $lineNode.SelectSingleNode("*[local-name()='style']"); if ($st) { $ln['style'] = $st.InnerText }
|
||
$o['line'] = $ln
|
||
}
|
||
$o['scaleColor'] = (Get-Child $lvn 'scaleColor')
|
||
$o['dayFormatRule'] = (Get-Child $lvn 'dayFormatRule')
|
||
$fmtNode = $lvn.SelectSingleNode("*[local-name()='format']")
|
||
if ($fmtNode) { $f = Get-LangText $fmtNode; if ($null -ne $f) { $o['format'] = $f } }
|
||
$labelsNode = $lvn.SelectSingleNode("*[local-name()='labels']")
|
||
if ($labelsNode) { $o['labels'] = [ordered]@{ ticks = (PLD-Int (Get-Child $labelsNode 'ticks')) } }
|
||
$o['backColor'] = (Get-Child $lvn 'backColor')
|
||
$o['textColor'] = (Get-Child $lvn 'textColor')
|
||
$o['showPereodicalLabels'] = (PLD-Bool (Get-Child $lvn 'showPereodicalLabels'))
|
||
return $o
|
||
}
|
||
function Build-PlannerTimeScale {
|
||
param($tsn)
|
||
$o = [ordered]@{}
|
||
$o['placement'] = (Get-Child $tsn 'placement')
|
||
$levels = New-Object System.Collections.ArrayList
|
||
foreach ($lvn in @($tsn.SelectNodes("*[local-name()='level']"))) { [void]$levels.Add((Build-PlannerLevel $lvn)) }
|
||
$o['levels'] = @($levels)
|
||
$o['transparent'] = (PLD-Bool (Get-Child $tsn 'transparent'))
|
||
$o['backColor'] = (Get-Child $tsn 'backColor')
|
||
$o['textColor'] = (Get-Child $tsn 'textColor')
|
||
$o['currentLevel'] = (PLD-Int (Get-Child $tsn 'currentLevel'))
|
||
return $o
|
||
}
|
||
function Build-PlannerSettings {
|
||
param($setNode)
|
||
$pl = [ordered]@{}
|
||
$itemNodes = @($setNode.SelectNodes("*[local-name()='item']"))
|
||
if ($itemNodes.Count -gt 0) {
|
||
$items = New-Object System.Collections.ArrayList
|
||
foreach ($itn in $itemNodes) { [void]$items.Add((Build-PlannerItem $itn)) }
|
||
$pl['items'] = @($items)
|
||
}
|
||
$dimNodes = @($setNode.SelectNodes("*[local-name()='dimension']"))
|
||
if ($dimNodes.Count -gt 0) {
|
||
$dims = New-Object System.Collections.ArrayList
|
||
foreach ($dn in $dimNodes) { [void]$dims.Add((Build-PlannerDimension $dn)) }
|
||
$pl['dimensions'] = @($dims)
|
||
}
|
||
$pl['borderColor'] = (Get-Child $setNode 'borderColor')
|
||
$pl['backColor'] = (Get-Child $setNode 'backColor')
|
||
$pl['textColor'] = (Get-Child $setNode 'textColor')
|
||
$pl['lineColor'] = (Get-Child $setNode 'lineColor')
|
||
$fnt = Build-PlannerFont ($setNode.SelectSingleNode("*[local-name()='font']")); if ($fnt) { $pl['font'] = $fnt }
|
||
$pl['beginOfRepresentationPeriod'] = (Get-Child $setNode 'beginOfRepresentationPeriod')
|
||
$pl['endOfRepresentationPeriod'] = (Get-Child $setNode 'endOfRepresentationPeriod')
|
||
$pl['alignElementsOfTimeScale'] = (PLD-Bool (Get-Child $setNode 'alignElementsOfTimeScale'))
|
||
$pl['displayTimeScaleWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayTimeScaleWrapHeaders'))
|
||
$pl['displayWrapHeaders'] = (PLD-Bool (Get-Child $setNode 'displayWrapHeaders'))
|
||
$wfNode = $setNode.SelectSingleNode("*[local-name()='timeScaleWrapHeadersFormat']")
|
||
if ($wfNode) { $wf = Get-LangText $wfNode; if ($null -ne $wf) { $pl['timeScaleWrapHeadersFormat'] = $wf } }
|
||
$pl['periodicVariantUnit'] = (Get-Child $setNode 'periodicVariantUnit')
|
||
$pl['periodicVariantRepetition'] = (PLD-Int (Get-Child $setNode 'periodicVariantRepetition'))
|
||
$pl['timeScaleWrapBeginIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapBeginIndent'))
|
||
$pl['timeScaleWrapEndIndent'] = (PLD-Int (Get-Child $setNode 'timeScaleWrapEndIndent'))
|
||
$tsNode = $setNode.SelectSingleNode("*[local-name()='timeScale']")
|
||
if ($tsNode) { $pl['timeScale'] = (Build-PlannerTimeScale $tsNode) }
|
||
$perNode = $setNode.SelectSingleNode("*[local-name()='period']")
|
||
if ($perNode) { $pl['period'] = [ordered]@{ begin = (Get-Child $perNode 'begin'); end = (Get-Child $perNode 'end') } }
|
||
$pl['displayCurrentDate'] = (PLD-Bool (Get-Child $setNode 'displayCurrentDate'))
|
||
$pl['itemsTimeRepresentation'] = (Get-Child $setNode 'itemsTimeRepresentation')
|
||
$pl['itemsBehaviorWhenSpaceInsufficient'] = (Get-Child $setNode 'itemsBehaviorWhenSpaceInsufficient')
|
||
$pl['autoMinColumnWidth'] = (PLD-Bool (Get-Child $setNode 'autoMinColumnWidth'))
|
||
$pl['autoMinRowHeight'] = (PLD-Bool (Get-Child $setNode 'autoMinRowHeight'))
|
||
$pl['minColumnWidth'] = (PLD-Int (Get-Child $setNode 'minColumnWidth'))
|
||
$pl['minRowHeight'] = (PLD-Int (Get-Child $setNode 'minRowHeight'))
|
||
$pl['fixDimensionsHeader'] = (Get-Child $setNode 'fixDimensionsHeader')
|
||
$pl['fixTimeScaleHeader'] = (Get-Child $setNode 'fixTimeScaleHeader')
|
||
$brd = Build-PlannerBorder ($setNode.SelectSingleNode("*[local-name()='border']")); if ($brd) { $pl['border'] = $brd }
|
||
$pl['newItemsTextType'] = (Get-Child $setNode 'newItemsTextType')
|
||
return $pl
|
||
}
|
||
|
||
# --- 5. Form-level assembly ---
|
||
$dsl = [ordered]@{}
|
||
|
||
$titleNode = $root.SelectSingleNode("lf:Title", $ns)
|
||
if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } }
|
||
|
||
# properties (прямые скаляры под <Form>, PascalCase → camelCase)
|
||
$KNOWN_FORM_PROPS = @('AutoTitle','ReportResult','DetailsData','ReportFormType','AutoShowState','ReportResultViewMode','ViewModeApplicationOnSetReportResult','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings','ScalingMode','VerticalSpacing','VariantAppearance')
|
||
$props = [ordered]@{}
|
||
foreach ($pn in $KNOWN_FORM_PROPS) {
|
||
$v = Get-Child $root $pn
|
||
if ($null -ne $v) {
|
||
$camel = $pn.Substring(0,1).ToLower() + $pn.Substring(1)
|
||
if ($v -eq 'true') { $props[$camel] = $true }
|
||
elseif ($v -eq 'false') { $props[$camel] = $false }
|
||
elseif ($v -match '^\d+$') { $props[$camel] = [int]$v }
|
||
else { $props[$camel] = $v }
|
||
}
|
||
}
|
||
# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим:
|
||
# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title);
|
||
# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить).
|
||
if ($dsl.Contains('title')) {
|
||
if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' }
|
||
elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') }
|
||
}
|
||
if ($props.Count -gt 0) { $dsl['properties'] = $props }
|
||
|
||
# MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок
|
||
$mdcb = $root.SelectSingleNode("lf:MobileDeviceCommandBarContent", $ns)
|
||
if ($mdcb) {
|
||
$names = New-Object System.Collections.ArrayList
|
||
foreach ($it in @($mdcb.SelectNodes("xr:Item", $ns))) {
|
||
$v = $it.SelectSingleNode("xr:Value", $ns); if ($v) { [void]$names.Add($v.InnerText) }
|
||
}
|
||
if ($names.Count -gt 0) { $dsl['mobileCommandBarContent'] = @($names) }
|
||
}
|
||
|
||
# excludedCommands (form-level <CommandSet>)
|
||
$csForm = $root.SelectSingleNode("lf:CommandSet", $ns)
|
||
if ($csForm) {
|
||
$excForm = New-Object System.Collections.ArrayList
|
||
foreach ($ec in @($csForm.SelectNodes("lf:ExcludedCommand", $ns))) { [void]$excForm.Add($ec.InnerText) }
|
||
if ($excForm.Count -gt 0) { $dsl['excludedCommands'] = @($excForm) }
|
||
}
|
||
|
||
# events (form-level)
|
||
$evForm = Get-Events $root $null
|
||
if ($evForm) {
|
||
# form-level: компилятор хранит как {Event: handler} напрямую
|
||
$evMap = [ordered]@{}
|
||
$evNode = $root.SelectSingleNode("lf:Events", $ns)
|
||
foreach ($e in @($evNode.SelectNodes("lf:Event", $ns))) { $evMap[$e.GetAttribute("name")] = $e.InnerText }
|
||
if ($evMap.Count -gt 0) { $dsl['events'] = $evMap }
|
||
}
|
||
|
||
# Зеркало компилятор-эвристики B3 (Compute-MainAcbAutofill): наличие cmdBar-элемента где-либо
|
||
# в дереве → форменный AutoCommandBar autofill=false. Нужно, чтобы предсказать вывод и выразить
|
||
# отклонение (голый корень при наличии cmdBar).
|
||
function Test-AnyCmdBar {
|
||
param($list)
|
||
if (-not $list) { return $false }
|
||
foreach ($e in $list) {
|
||
if ($e -is [System.Collections.IDictionary] -and $e.Contains('cmdBar')) { return $true }
|
||
if ($e -is [System.Collections.IDictionary]) {
|
||
if ($e.Contains('children') -and (Test-AnyCmdBar $e['children'])) { return $true }
|
||
if ($e.Contains('columns') -and (Test-AnyCmdBar $e['columns'])) { return $true }
|
||
}
|
||
}
|
||
return $false
|
||
}
|
||
|
||
# elements (+ форменный AutoCommandBar как autoCmdBar-элемент, если у него есть содержимое/отклонение)
|
||
$elemList = New-Object System.Collections.ArrayList
|
||
$elements = Decompile-Children $root
|
||
$formHasCmdBar = Test-AnyCmdBar $elements
|
||
$acb = $root.SelectSingleNode("lf:AutoCommandBar", $ns)
|
||
if ($acb) {
|
||
$haln = Get-Child $acb 'HorizontalAlign'
|
||
$acbAutofill = Get-Child $acb 'Autofill'
|
||
$acbKids = Decompile-Children $acb
|
||
$acbObj = $null
|
||
if ($haln -or ($acbAutofill -eq 'false') -or $acbKids) {
|
||
$acbObj = [ordered]@{}
|
||
$acbObj['autoCmdBar'] = $acb.GetAttribute("name")
|
||
if ($haln) { $acbObj['horizontalAlign'] = $haln }
|
||
if ($acbAutofill -eq 'false') { $acbObj['autofill'] = $false }
|
||
if ($acbKids) { $acbObj['children'] = $acbKids }
|
||
} elseif ($formHasCmdBar -and $null -eq $acbAutofill) {
|
||
# Корень голый (autofill=true по умолчанию), но эвристика B3 дала бы false (есть cmdBar) → маркер
|
||
$acbObj = [ordered]@{}
|
||
$acbObj['autoCmdBar'] = $acb.GetAttribute("name")
|
||
$acbObj['autofill'] = $true
|
||
}
|
||
if ($acbObj) { [void]$elemList.Add($acbObj) }
|
||
}
|
||
if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } }
|
||
if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) }
|
||
|
||
# attributes
|
||
# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3.
|
||
function Test-IsObjectLikeTypeDec([string]$type) {
|
||
if ([string]::IsNullOrEmpty($type)) { return $false }
|
||
if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true }
|
||
return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.')
|
||
}
|
||
$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns)
|
||
if ($attrsNode) {
|
||
$attrs = New-Object System.Collections.ArrayList
|
||
# Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного <MainAttribute> И ровно
|
||
# один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет)
|
||
# → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно.
|
||
$allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns))
|
||
$anyMainAttr = $false; $objLikeNodes = @()
|
||
foreach ($an in $allAttrNodes) {
|
||
if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true }
|
||
$atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns))
|
||
if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an }
|
||
}
|
||
$suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null }
|
||
foreach ($a in $allAttrNodes) {
|
||
$ao = [ordered]@{}
|
||
$ao['name'] = $a.GetAttribute("name")
|
||
$ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty }
|
||
# valueType: <Settings xsi:type="v8:TypeDescription"> — уточнение типа значений ValueList
|
||
# (та же грамматика типа). Дин-список Settings (xsi:type="DynamicList") обрабатывается отдельно.
|
||
$setNode = $a.SelectSingleNode("lf:Settings", $ns)
|
||
if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'TypeDescription$') {
|
||
$vt = Decompile-Type $setNode
|
||
$ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер ""
|
||
}
|
||
# Planner design-time <Settings xsi:type="pl:Planner"> → объект planner (полный захват).
|
||
elseif ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'Planner$') {
|
||
$ao['planner'] = Build-PlannerSettings $setNode
|
||
}
|
||
if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true }
|
||
elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false }
|
||
$vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw }
|
||
$ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed }
|
||
# Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок
|
||
# из имени. Поэтому: нет <Title> → суппресс-маркер ''; ru-only == авто-вывод → опускаем
|
||
# ключ (компилятор воспроизведёт); иначе → явный заголовок.
|
||
$isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title
|
||
$tNode = $a.SelectSingleNode("lf:Title", $ns)
|
||
if ($tNode) {
|
||
$t = Get-LangText $tNode
|
||
if ($null -ne $t) {
|
||
if ($isMain -or -not ($t -is [string]) -or $t -ne (Title-FromName $ao['name'])) { $ao['title'] = $t }
|
||
}
|
||
} elseif (-not $isMain) {
|
||
$ao['title'] = ''
|
||
}
|
||
# SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved:
|
||
# Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал
|
||
# тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute).
|
||
if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true }
|
||
elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false }
|
||
# Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true;
|
||
# иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив.
|
||
$saveNode = $a.SelectSingleNode("lf:Save", $ns)
|
||
if ($saveNode) {
|
||
$nm = "$($ao['name'])"
|
||
$flds = @($saveNode.SelectNodes("lf:Field", $ns) | ForEach-Object { $_.InnerText })
|
||
if ($flds.Count -eq 1 -and $flds[0] -eq $nm) {
|
||
$ao['save'] = $true
|
||
} elseif ($flds.Count -gt 0) {
|
||
$stripped = @($flds | ForEach-Object {
|
||
if ($_ -match "^$([regex]::Escape($nm))\.(.+)$") { $matches[1] } else { $_ }
|
||
})
|
||
if ($stripped.Count -eq 1) { $ao['save'] = $stripped[0] } else { $ao['save'] = $stripped }
|
||
}
|
||
}
|
||
$fc = Get-Child $a 'FillCheck'; if ($fc) { $ao['fillCheck'] = $fc }
|
||
$afo = Decompile-FunctionalOptions $a; if ($afo) { $ao['functionalOptions'] = $afo }
|
||
$colsNode = $a.SelectSingleNode("lf:Columns", $ns)
|
||
if ($colsNode) {
|
||
$cols = New-Object System.Collections.ArrayList
|
||
foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) { [void]$cols.Add((Decompile-AttrColumn $c)) }
|
||
if ($cols.Count -gt 0) { $ao['columns'] = @($cols) }
|
||
# AdditionalColumns: доп. колонки табличных частей объекта (группа на табличную часть)
|
||
$addNodes = @($colsNode.SelectNodes("lf:AdditionalColumns", $ns))
|
||
if ($addNodes.Count -gt 0) {
|
||
$addList = New-Object System.Collections.ArrayList
|
||
foreach ($an in $addNodes) {
|
||
$acObj = [ordered]@{}; $acObj['table'] = $an.GetAttribute("table")
|
||
$acCols = New-Object System.Collections.ArrayList
|
||
foreach ($c in @($an.SelectNodes("lf:Column", $ns))) { [void]$acCols.Add((Decompile-AttrColumn $c)) }
|
||
$acObj['columns'] = @($acCols)
|
||
[void]$addList.Add($acObj)
|
||
}
|
||
$ao['additionalColumns'] = @($addList)
|
||
}
|
||
}
|
||
# UseAlways: поля, всегда читаемые. Префикс "ИмяРеквизита." снимаем.
|
||
# ValueTable (есть columns): useAlways:true на совпавшей колонке; остальные → массив атрибута.
|
||
# Дин-список/прочие (нет columns): массив useAlways на атрибуте.
|
||
$uaNode = $a.SelectSingleNode("lf:UseAlways", $ns)
|
||
if ($uaNode) {
|
||
$prefix = "$($ao['name'])."
|
||
$shorts = New-Object System.Collections.ArrayList
|
||
foreach ($fn in @($uaNode.SelectNodes("lf:Field", $ns))) {
|
||
$t = $fn.InnerText.Trim()
|
||
# Снимаем префикс "ИмяРеквизита.". Маркер "~" (query-поле дин-списка) сохраняем,
|
||
# префикс снимаем ПОСЛЕ него: ~Список.Остановлен → ~Остановлен (компилятор развернёт обратно).
|
||
if ($t.StartsWith('~')) {
|
||
$rest = $t.Substring(1)
|
||
if ($rest.StartsWith($prefix)) { $rest = $rest.Substring($prefix.Length) }
|
||
$t = "~$rest"
|
||
} elseif ($t.StartsWith($prefix)) {
|
||
$t = $t.Substring($prefix.Length)
|
||
}
|
||
[void]$shorts.Add($t)
|
||
}
|
||
if ($ao.Contains('columns')) {
|
||
$rest = New-Object System.Collections.ArrayList
|
||
foreach ($s in $shorts) {
|
||
$col = $ao['columns'] | Where-Object { $_['name'] -eq $s } | Select-Object -First 1
|
||
if ($col) { $col['useAlways'] = $true } else { [void]$rest.Add($s) }
|
||
}
|
||
if ($rest.Count -gt 0) { $ao['useAlways'] = @($rest) }
|
||
} elseif ($shorts.Count -gt 0) {
|
||
$ao['useAlways'] = @($shorts)
|
||
}
|
||
}
|
||
# Settings динамического списка (только xsi:type=DynamicList; Planner/TypeDescription — выше)
|
||
$setNode = $a.SelectSingleNode("lf:Settings", $ns)
|
||
if ($setNode -and $setNode.GetAttribute("type", $NS_XSI) -match 'DynamicList$') {
|
||
$so = [ordered]@{}
|
||
# AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть».
|
||
$afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') }
|
||
$mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt }
|
||
$qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns)
|
||
if ($qtNode -and $qtNode.InnerText) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" }
|
||
# DynamicDataRead: дефолт true → эмитим только false
|
||
if ((Get-Child $setNode 'DynamicDataRead') -eq 'false') { $so['dynamicDataRead'] = $false }
|
||
# Явные поля набора (редко, ~4.5%) — захват только при наличии Field
|
||
$fieldNodes = @($setNode.SelectNodes("lf:Field", $ns))
|
||
if ($fieldNodes.Count -gt 0) {
|
||
$fields = New-Object System.Collections.ArrayList
|
||
foreach ($fn in $fieldNodes) {
|
||
$fo = [ordered]@{}
|
||
$fld = Get-Child $fn 'field'
|
||
$dp = Get-Child $fn 'dataPath'
|
||
if ($fld) { $fo['field'] = $fld }
|
||
if ($dp -and $dp -ne $fld) { $fo['dataPath'] = $dp }
|
||
$ftn = $fn.SelectSingleNode("dcssch:title", $ns)
|
||
if ($ftn) { $t = Get-LangText $ftn; if ($null -ne $t) { $fo['title'] = $t } }
|
||
[void]$fields.Add($fo)
|
||
}
|
||
$so['fields'] = @($fields)
|
||
}
|
||
# Schema-параметры дин-списка (прямые <Parameter> под Settings, не в ListSettings)
|
||
$paramNodes = @($setNode.SelectNodes("lf:Parameter", $ns))
|
||
if ($paramNodes.Count -gt 0) {
|
||
$dlPars = New-Object System.Collections.ArrayList
|
||
foreach ($pn in $paramNodes) { [void]$dlPars.Add((Build-DLParameter $pn)) }
|
||
$so['parameters'] = @($dlPars)
|
||
}
|
||
# ListSettings: пустой скелет (только viewMode+GUID) опускаем — компилятор
|
||
# регенерит каноничный скелет. Захватываем только контейнеры с реальными
|
||
# dcsset:item (filter/order/conditionalAppearance) в формат компилятора.
|
||
$lsNode = $setNode.SelectSingleNode("lf:ListSettings", $ns)
|
||
if ($lsNode) {
|
||
$fNode = $lsNode.SelectSingleNode("dcsset:filter", $ns)
|
||
if ($fNode -and $fNode.SelectSingleNode("dcsset:item", $ns)) {
|
||
$flt = @()
|
||
foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) {
|
||
$bi = (Build-FilterItem -itemNode $fc -loc "settings/filter")
|
||
if ($null -ne $bi) { $flt += $bi }
|
||
}
|
||
if ($flt.Count -gt 0) { $so['filter'] = @($flt) }
|
||
}
|
||
$oNode = $lsNode.SelectSingleNode("dcsset:order", $ns)
|
||
if ($oNode -and $oNode.SelectSingleNode("dcsset:item", $ns)) {
|
||
$ord = Build-Order -ordNode $oNode -loc "settings/order"
|
||
if (@($ord).Count -gt 0) { $so['order'] = @($ord) }
|
||
}
|
||
$caNode = $lsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns)
|
||
if ($caNode -and $caNode.SelectSingleNode("dcsset:item", $ns)) {
|
||
$ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance"
|
||
if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) }
|
||
}
|
||
# Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные).
|
||
# Канон → $null (компилятор регенерит полный скелет, как раньше).
|
||
$lsShape = Get-ListSettingsShape $lsNode
|
||
if ($null -ne $lsShape) { $so['listSettings'] = $lsShape }
|
||
}
|
||
if ($so.Count -gt 0) { $ao['settings'] = $so }
|
||
}
|
||
[void]$attrs.Add($ao)
|
||
}
|
||
if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) }
|
||
}
|
||
|
||
# conditionalAppearance формы (<ConditionalAppearance> — последний child <Attributes>;
|
||
# та же DCS-грамматика, что settings.conditionalAppearance → переиспользуем Build-ConditionalAppearance)
|
||
if ($attrsNode) {
|
||
$caNode = $attrsNode.SelectSingleNode("lf:ConditionalAppearance", $ns)
|
||
if ($caNode) {
|
||
$ca = Build-ConditionalAppearance -caNode $caNode -loc "form/conditionalAppearance"
|
||
if (@($ca).Count -gt 0) { $dsl['conditionalAppearance'] = @($ca) }
|
||
}
|
||
}
|
||
|
||
# parameters
|
||
$parsNode = $root.SelectSingleNode("lf:Parameters", $ns)
|
||
if ($parsNode) {
|
||
$pars = New-Object System.Collections.ArrayList
|
||
foreach ($p in @($parsNode.SelectNodes("lf:Parameter", $ns))) {
|
||
$po = [ordered]@{}; $po['name'] = $p.GetAttribute("name")
|
||
$ty = Decompile-Type ($p.SelectSingleNode("lf:Type", $ns)); if ($ty) { $po['type'] = $ty }
|
||
if ((Get-Child $p 'KeyParameter') -eq 'true') { $po['key'] = $true }
|
||
[void]$pars.Add($po)
|
||
}
|
||
if ($pars.Count -gt 0) { $dsl['parameters'] = @($pars) }
|
||
}
|
||
|
||
# commands
|
||
$cmdsNode = $root.SelectSingleNode("lf:Commands", $ns)
|
||
if ($cmdsNode) {
|
||
$cmds = New-Object System.Collections.ArrayList
|
||
foreach ($c in @($cmdsNode.SelectNodes("lf:Command", $ns))) {
|
||
$co = [ordered]@{}; $co['name'] = $c.GetAttribute("name")
|
||
$act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act }
|
||
if ((Get-Child $c 'ModifiesSavedData') -eq 'true') { $co['modifiesSavedData'] = $true }
|
||
$tNode = $c.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } }
|
||
$ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } }
|
||
$us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us }
|
||
$cfo = Decompile-FunctionalOptions $c; if ($cfo) { $co['functionalOptions'] = $cfo }
|
||
$cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru }
|
||
# Используемая таблица — ссылка по имени элемента-таблицы (<AssociatedTableElementId xsi:type="xs:string">Имя</…>)
|
||
$ate = Get-Child $c 'AssociatedTableElementId'; if ($ate) { $co['table'] = $ate }
|
||
$sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc }
|
||
$ref = $c.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $co['picture'] = $ref.InnerText }
|
||
# Дефолт у picture команды = true → фиксируем только отклонение false (true опускаем)
|
||
$lt = $c.SelectSingleNode("lf:Picture/xr:LoadTransparent", $ns); if ($lt -and $lt.InnerText -eq 'false') { $co['loadTransparent'] = $false }
|
||
$rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep }
|
||
[void]$cmds.Add($co)
|
||
}
|
||
if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) }
|
||
}
|
||
|
||
# commandInterface (форменный <CommandInterface> — последний дочерний Form)
|
||
$ci = Decompile-CommandInterface
|
||
if ($null -ne $ci) { $dsl['commandInterface'] = $ci }
|
||
|
||
# --- 6. Output ---
|
||
$json = ConvertTo-CompactJson -obj $dsl
|
||
if ($OutputPath) {
|
||
[System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false)))
|
||
Save-QueryFiles
|
||
Write-Host "form-decompile: $OutputPath"
|
||
} else {
|
||
Write-Output $json
|
||
}
|