feat(form-decompile): MVP-декомпилятор Form.xml→JSON + компактный вывод (draft, ring-ограничен)

Декомпилятор управляемой формы в формат form-compile (инверсия компилятора):
метаданные, реквизиты, параметры, команды, события, рекурсия ChildItems по
базовым типам, strip companions, инверсия типов и авто-имён обработчиков.
Компактный вывод тем же сериализатором, что skd-decompile (inline в пределах
lineLimit=120). Ring-ограничен (disable-model-invocation): раундтрип не
гарантируется, риск неполноты на пользователе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-04 14:37:54 +03:00
parent 6d119eb473
commit 7c38422e2c
2 changed files with 628 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
---
name: form-decompile
description: Декомпиляция управляемой формы 1С (Form.xml) в JSON-черновик в формате form-compile. Используй для scaffold новой формы по образцу или структурного рефакторинга. Не для точечных правок
argument-hint: <FormPath> [-OutputPath <out.json>]
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /form-decompile — JSON-черновик из Form.xml управляемой формы
Читает Form.xml и эмитит компактный JSON в формате `form-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь».
## Когда использовать
- **Scaffold новой формы по образцу** — взять существующую форму, получить JSON, поправить и скомпилировать в новую.
- **Структурный рефакторинг** — перебрать дерево элементов, реквизиты, команды.
## Когда **не** использовать
- **Точечные правки готовой формы** (добавить элемент, реквизит, команду) → `/form-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает форму целиком, может терять непокрытые конструкции и даёт большой diff. `/form-edit` правит адресно.
## Параметры
| Параметр | Описание |
|----------|----------|
| `FormPath` | Путь к Form.xml (обязательный) |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-decompile.ps1" -FormPath "<Form.xml>" -OutputPath "<out.json>"
```
## Что получаешь
JSON-черновик в формате `/form-compile`**не полное обратимое представление формы**. Раундтрип `xml → json → xml` пока не гарантируется: ряд конструкций управляемой формы DSL не покрывает.
- **Покрытые узлы** — метаданные формы, реквизиты, параметры, команды, события, дерево базовых элементов (группы, поля, таблицы, страницы, кнопки и т.п.) ложатся в JSON как обычные узлы DSL.
- **Непокрытое теряется молча** — конструкции вне зоны поддержки (богатые настройки полей, источники строк поиска, командные интерфейсы и др.) при декомпиляции опускаются. Поэтому навык **исключён из автоматического использования моделью** (`disable-model-invocation: true`): риск неполноты принимает на себя пользователь, явно вызывая `/form-decompile`.
- **Критичные конструкции** (`CommandInterface`, `ConditionalAppearance`, неизвестный тип элемента, не-Form root) — скрипт падает с ненулевым кодом и сообщением в stderr; такую форму как образец брать нельзя, для правок — `/form-edit`.
## Workflow
1. `/form-decompile <Form.xml> -OutputPath draft.json` — получить черновик.
2. Поправить JSON под задачу.
3. `/form-compile -JsonPath draft.json -OutputPath new/Form.xml` — собрать обратно.
4. `/form-validate` + `/form-info` — проверить результат.
@@ -0,0 +1,578 @@
# form-decompile v0.2 — 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 = 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)
# --- 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
}
foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='CommandInterface']")) { Fail-Ring3 -kind "CommandInterface" -loc "form/CommandInterface" }
foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='ConditionalAppearance']")) { Fail-Ring3 -kind "ConditionalAppearance" -loc "form/ConditionalAppearance" }
# --- 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-элементы (авто-генерируемые компилятором) — пропускаем при обходе детей.
$COMPANION_TAGS = @('ContextMenu','ExtendedTooltip','AutoCommandBar','SearchStringAddition','ViewStatusAddition','SearchControlAddition')
# Извлечь мультиязычный 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
}
# Прочитать дочерний скаляр (по 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 }
}
}
# Суффиксы авто-имён обработчиков (инверсия компилятора)
$HANDLER_SUFFIX = @{
'OnChange'='ПриИзменении'; 'StartChoice'='НачалоВыбора'; 'ChoiceProcessing'='ОбработкаВыбора';
'AutoComplete'='АвтоПодбор'; 'Clearing'='Очистка'; 'Opening'='Открытие'; 'Click'='Нажатие';
'OnActivateRow'='ПриАктивизацииСтроки'; 'BeforeAddRow'='ПередНачаломДобавления';
'BeforeDeleteRow'='ПередУдалением'; 'BeforeRowChange'='ПередНачаломИзменения';
'OnStartEdit'='ПриНачалеРедактирования'; 'OnEndEdit'='ПриОкончанииРедактирования';
'Selection'='ВыборСтроки'; 'OnCurrentPageChange'='ПриСменеСтраницы'; 'TextEditEnd'='ОкончаниеВводаТекста';
'URLProcessing'='ОбработкаНавигационнойСсылки'; 'DragStart'='НачалоПеретаскивания'; 'Drag'='Перетаскивание';
'DragCheck'='ПроверкаПеретаскивания'; 'Drop'='Помещение'; 'AfterDeleteRow'='ПослеУдаления'
}
# Разобрать <Events> элемента → { on:[...], handlers:{...} } с учётом авто-имён
function Get-Events {
param($node, [string]$elName)
$ev = $node.SelectSingleNode("lf:Events", $ns)
if (-not $ev) { return $null }
$on = New-Object System.Collections.ArrayList
$handlers = [ordered]@{}
foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) {
$evName = $e.GetAttribute("name")
$handler = $e.InnerText
$auto = if ($HANDLER_SUFFIX.ContainsKey($evName) -and $elName) { "$elName$($HANDLER_SUFFIX[$evName])" } else { $null }
if ($auto -and $handler -eq $auto) {
[void]$on.Add($evName)
} else {
$handlers[$evName] = $handler
}
}
$res = [ordered]@{}
if ($on.Count -gt 0) { $res['on'] = @($on) }
if ($handlers.Count -gt 0) { $res['handlers'] = $handlers }
if ($res.Count -eq 0) { return $null }
return $res
}
# Общие свойства элемента (visible/enabled/readonly/title/events) → в hash
function Add-CommonProps {
param($obj, $node, [string]$elName)
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 }
$titleNode = $node.SelectSingleNode("lf:Title", $ns)
if ($titleNode) {
$t = Get-LangText $titleNode
if ($null -ne $t) { $obj['title'] = $t }
$fmt = $titleNode.GetAttribute("formatted")
if ($fmt -eq 'true') { $obj['titleFormatted'] = $true } elseif ($fmt -eq 'false') { $obj['titleFormatted'] = $false }
}
$ev = Get-Events $node $elName
if ($ev) {
if ($ev.Contains('on')) { $obj['on'] = $ev['on'] }
if ($ev.Contains('handlers')) { $obj['handlers'] = $ev['handlers'] }
}
}
# --- 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
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" }
}
'^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)" }
}
'^xs:boolean$' { $short = "boolean" }
'^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' } }
}
'^v8:ValueListType$' { $short = 'ValueList' }
'^(v8|v8ui|cfg):(.+)$' { $short = $matches[2] }
default { $short = $raw }
}
[void]$parts.Add($short)
}
if ($parts.Count -eq 0) { return $null }
if ($parts.Count -eq 1) { return $parts[0] }
return ($parts -join ' | ')
}
# --- 4. Element dispatch ---
$ELEMENT_KEY = @{
'UsualGroup'='group'; 'ColumnGroup'='columnGroup'; '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'
}
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)
}
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' {
$g = Get-Child $node 'Group'
$gmap = @{ 'Horizontal'='horizontal'; 'Vertical'='vertical'; 'AlwaysHorizontal'='alwaysHorizontal'; 'AlwaysVertical'='alwaysVertical' }
$behavior = Get-Child $node 'Behavior'
if ($behavior -eq 'Collapsible') { $obj[$key] = 'collapsible' }
elseif ($g -and $gmap.ContainsKey($g)) { $obj[$key] = $gmap[$g] }
else { $obj[$key] = 'vertical' }
$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 }
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' {
$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] = 'horizontal' }
$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 }
$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 } }
foreach ($p in @('ChoiceButton','ClearButton','SpinButton','DropListButton')) {
$v = Get-Child $node $p; if ($null -ne $v) { $obj[($p.Substring(0,1).ToLower()+$p.Substring(1))] = (To-Bool $v) }
}
}
'CheckBoxField' {
$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() }
}
'RadioButtonField' {
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
$rbt = Get-Child $node 'RadioButtonType'; if ($rbt) { $obj['radioButtonType'] = $rbt }
$cc = Get-Child $node 'ColumnsCount'; if ($cc) { $obj['columnsCount'] = [int]$cc }
$cl = $node.SelectSingleNode("lf:ChoiceList", $ns)
if ($cl) {
$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
}
if ($presNode) { $p = Get-LangText $presNode; if ($p) { $ci['presentation'] = $p } }
[void]$items.Add($ci)
}
if ($items.Count -gt 0) { $obj['choiceList'] = @($items) }
}
}
'LabelDecoration' {
$obj[$key] = $name
Add-CommonProps $obj $node $name
if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
}
'LabelField' {
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
if ((Get-Child $node 'Hyperlink') -eq 'true') { $obj['hyperlink'] = $true }
}
'PictureDecoration' {
$obj[$key] = $name
Add-CommonProps $obj $node $name
$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['src'] = $ref.InnerText }
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
$ref = $node.SelectSingleNode("lf:ValuesPicture/xr:Ref", $ns); if ($ref) { $obj['valuesPicture'] = $ref.InnerText }
}
'CalendarField' {
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
}
'Table' {
$obj[$key] = $name
$dp = Get-Child $node 'DataPath'; if ($dp) { $obj['path'] = $dp }
Add-CommonProps $obj $node $name
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
if ((Get-Child $node 'ChangeRowSet') -eq 'true') { $obj['changeRowSet'] = $true }
if ((Get-Child $node 'ChangeRowOrder') -eq 'true') { $obj['changeRowOrder'] = $true }
if ((Get-Child $node 'Header') -eq 'false') { $obj['header'] = $false }
if ((Get-Child $node 'Footer') -eq 'true') { $obj['footer'] = $true }
$cbl = Get-Child $node 'CommandBarLocation'; if ($cbl) { $obj['commandBarLocation'] = $cbl }
$cols = Decompile-Children $node
if ($cols) { $obj['columns'] = $cols }
}
'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] }
$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['command'] = $cmd }
}
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 }
$ref = $node.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $obj['picture'] = $ref.InnerText }
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
$lic = Get-Child $node 'LocationInCommandBar'; if ($lic) { $obj['locationInCommandBar'] = $lic }
}
'CommandBar' {
$obj[$key] = $name
Add-CommonProps $obj $node $name
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 }
$rep = Get-Child $node 'Representation'; if ($rep) { $obj['representation'] = $rep }
$kids = Decompile-Children $node
if ($kids) { $obj['children'] = $kids }
}
}
return $obj
}
# --- 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','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems')
$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=false при наличии title — это инъекция компилятора, опускаем (валидируем раундтрипом)
if ($dsl.Contains('title') -and $props.Contains('autoTitle') -and $props['autoTitle'] -eq $false) { $props.Remove('autoTitle') }
if ($props.Count -gt 0) { $dsl['properties'] = $props }
# 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 }
}
# elements
$elements = Decompile-Children $root
if ($elements) { $dsl['elements'] = $elements }
# attributes
$attrsNode = $root.SelectSingleNode("lf:Attributes", $ns)
if ($attrsNode) {
$attrs = New-Object System.Collections.ArrayList
foreach ($a in @($attrsNode.SelectNodes("lf:Attribute", $ns))) {
$ao = [ordered]@{}
$ao['name'] = $a.GetAttribute("name")
$ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty }
if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true }
$tNode = $a.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $ao['title'] = $t } }
if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true }
$fc = Get-Child $a 'FillChecking'; if ($fc) { $ao['fillChecking'] = $fc }
$colsNode = $a.SelectSingleNode("lf:Columns", $ns)
if ($colsNode) {
$cols = New-Object System.Collections.ArrayList
foreach ($c in @($colsNode.SelectNodes("lf:Column", $ns))) {
$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 } }
[void]$cols.Add($co)
}
if ($cols.Count -gt 0) { $ao['columns'] = @($cols) }
}
[void]$attrs.Add($ao)
}
if ($attrs.Count -gt 0) { $dsl['attributes'] = @($attrs) }
}
# 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 }
$tNode = $c.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } }
$sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc }
$ref = $c.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $co['picture'] = $ref.InnerText }
$rep = Get-Child $c 'Representation'; if ($rep) { $co['representation'] = $rep }
[void]$cmds.Add($co)
}
if ($cmds.Count -gt 0) { $dsl['commands'] = @($cmds) }
}
# --- 6. Output ---
$json = ConvertTo-CompactJson -obj $dsl
if ($OutputPath) {
[System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false)))
Write-Host "form-decompile: $OutputPath"
} else {
Write-Output $json
}