Files
cc-1c-skills/.claude/skills/meta-decompile/scripts/meta-decompile.ps1
T
Nick ShirokovandClaude Opus 4.8 90a05a58e2 feat(meta-compile,meta-decompile): «Только обороты» как токен-признак субконто (v1.39/v0.28)
«Только обороты» (<Turnover>) — предопределённый (встроенный) признак учёта субконто. Теперь он
выражается токеном `Turnover` (синонимы ТолькоОбороты/«Только обороты») в том же списке признаков,
что и добавленные: "Номенклатура | Turnover, Суммовой". Строковая форма стала полностью
самодостаточной — объектная {type, turnover?, flags?} остаётся принимаемым эквивалентом.

Компилятор вынимает токен turnover из списка flags (регистронезависимо) → <Turnover>true.
Декомпилятор всегда пишет строкой, ставя `Turnover` первым при Turnover=true (объектную форму
больше не порождает). Роундтрип 3/3 match; обе формы + смешанный массив проверены, ps1↔py identical.
Регресс 47/47 ps1+py. spec §7.2c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:30:58 +03:00

865 lines
51 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# meta-decompile v0.28 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts. Инверс meta-compile (omit-on-default: ключ эмитим только
# когда значение в XML отличается от умолчания компилятора). Неподдерживаемый тип / не-MetaDataObject
# root → exit 3 (ring3, как form-decompile).
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path -LiteralPath $ObjectPath)) {
[Console]::Error.WriteLine("meta-decompile: файл не найден: $ObjectPath"); exit 2
}
# --- JSON-эмиттер (контроль порядка/массивов/кириллицы) ---
function ConvertTo-CompactJson {
param($node, [int]$depth = 0)
$pad = " " * $depth
$pad1 = " " * ($depth + 1)
if ($null -eq $node) { return "null" }
if ($node -is [bool]) { return $(if ($node) { "true" } else { "false" }) }
if ($node -is [int] -or $node -is [long] -or $node -is [double]) { return "$node" }
if ($node -is [System.Collections.IDictionary]) {
if ($node.Count -eq 0) { return "{}" }
$items = @()
foreach ($k in $node.Keys) {
$items += "$pad1$(Quote-Json ([string]$k)): $(ConvertTo-CompactJson $node[$k] ($depth + 1))"
}
return "{`n" + ($items -join ",`n") + "`n$pad}"
}
if ($node -is [System.Collections.IList]) {
if ($node.Count -eq 0) { return "[]" }
# Массив скаляров-строк — компактно в строку; массив объектов — по строкам.
$allScalar = $true
foreach ($e in $node) { if ($e -is [System.Collections.IDictionary] -or $e -is [System.Collections.IList]) { $allScalar = $false; break } }
if ($allScalar) {
$items = @(); foreach ($e in $node) { $items += (ConvertTo-CompactJson $e ($depth + 1)) }
return "[" + ($items -join ", ") + "]"
}
$items = @(); foreach ($e in $node) { $items += "$pad1$(ConvertTo-CompactJson $e ($depth + 1))" }
return "[`n" + ($items -join ",`n") + "`n$pad]"
}
return Quote-Json ([string]$node)
}
function Quote-Json {
param([string]$s)
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append('"')
foreach ($ch in $s.ToCharArray()) {
switch ($ch) {
'"' { [void]$sb.Append('\"') }
'\' { [void]$sb.Append('\\') }
"`n" { [void]$sb.Append('\n') }
"`r" { [void]$sb.Append('\r') }
"`t" { [void]$sb.Append('\t') }
default {
if ([int]$ch -lt 32) { [void]$sb.Append(('\u{0:x4}' -f [int]$ch)) }
else { [void]$sb.Append($ch) }
}
}
}
[void]$sb.Append('"')
return $sb.ToString()
}
# --- XML загрузка + namespace manager ---
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load((Resolve-Path -LiteralPath $ObjectPath).Path)
$nsm = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$nsm.AddNamespace('md', 'http://v8.1c.ru/8.3/MDClasses')
$nsm.AddNamespace('v8', 'http://v8.1c.ru/8.1/data/core')
$nsm.AddNamespace('xr', 'http://v8.1c.ru/8.3/xcf/readable')
$nsm.AddNamespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
$nsm.AddNamespace('app', 'http://v8.1c.ru/8.2/managed-application/core')
$rootEl = $doc.DocumentElement
if ($rootEl.LocalName -ne 'MetaDataObject') {
[Console]::Error.WriteLine("meta-decompile: ожидался root <MetaDataObject>, получен <$($rootEl.LocalName)>"); exit 3
}
# Первый элемент-потомок MetaDataObject = объект; его LocalName = тип.
$objNode = $null
foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode = $c; break } }
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
$objType = $objNode.LocalName
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts')) {
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts)"); exit 3
}
$props = $objNode.SelectSingleNode('md:Properties', $nsm)
function P { param([string]$tag) $n = $props.SelectSingleNode("md:$tag", $nsm); if ($n) { return $n.InnerText } else { return $null } }
# --- Synonym (ru) ---
function Get-MLru {
param($node)
if (-not $node) { return $null }
$it = $node.SelectSingleNode("v8:item[v8:lang='ru']/v8:content", $nsm)
if ($it) { return $it.InnerText }
return $null
}
# ML-значение → строка (если единственный item ru) ЛИБО [ordered]{lang:content} (мультиязычно, порядок из XML).
# null если контента нет. Компактная строка для ru-only, объект для мультиязычных.
function Get-MLValue {
param($node)
if (-not $node) { return $null }
$items = @($node.SelectNodes('v8:item', $nsm))
if ($items.Count -eq 0) { return $null }
if ($items.Count -eq 1) {
$lang = $items[0].SelectSingleNode('v8:lang', $nsm).InnerText
$content = $items[0].SelectSingleNode('v8:content', $nsm).InnerText
# Единственный ru-item: пустое содержимое ≡ отсутствие значения → $null (иначе tooltip:"" ≠ self-close).
if ($lang -eq 'ru') { if ($content -eq '') { return $null } else { return $content } }
}
$o = [ordered]@{}
foreach ($it in $items) {
$l = $it.SelectSingleNode('v8:lang', $nsm).InnerText
$c = $it.SelectSingleNode('v8:content', $nsm).InnerText
$o[$l] = $c
}
return $o
}
# Авто-синоним: точное зеркало Split-CamelCase из meta-compile (стр.354). Совпало → ключ опускаем.
# ВАЖНО: логика должна совпадать байт-в-байт с компилятором, иначе ложные «синоним==авто» → диффы.
function Split-CamelWords {
param([string]$name)
if (-not $name) { return $name }
$result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2')
$result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2')
if ($result.Length -gt 1) { $result = $result.Substring(0,1) + $result.Substring(1).ToLower() }
return $result
}
$objName = P 'Name'
# --- Тип реквизита: <Type> → shorthand-строка ---
function Strip-NsPrefix { param([string]$s) if ($s -match ':') { return ($s -split ':', 2)[1] } else { return $s } }
function Get-TypeShorthand {
param($typeNode)
if (-not $typeNode) { return "" }
$parts = @()
$children = @($typeNode.ChildNodes | Where-Object { $_.NodeType -eq 'Element' })
for ($i = 0; $i -lt $children.Count; $i++) {
$c = $children[$i]
$ln = $c.LocalName
if ($ln -eq 'Type') {
$raw = $c.InnerText.Trim()
$next = if ($i + 1 -lt $children.Count) { $children[$i+1] } else { $null }
switch -regex ($raw) {
'(^|:)boolean$' { $parts += 'Boolean'; break }
'(^|:)string$' {
$len = '10'; $al = ''
$sq = if ($next -and $next.LocalName -eq 'StringQualifiers') { $next } else { $typeNode.SelectSingleNode('v8:StringQualifiers', $nsm) }
if ($sq) {
$l = $sq.SelectSingleNode('v8:Length', $nsm); if ($l) { $len = $l.InnerText }
$aln = $sq.SelectSingleNode('v8:AllowedLength', $nsm); if ($aln -and $aln.InnerText -eq 'Fixed') { $al = ',fixed' }
}
$parts += "String($len$al)"; break
}
'(^|:)decimal$' {
$d = '10'; $f = '0'; $sign = ''
$nq = if ($next -and $next.LocalName -eq 'NumberQualifiers') { $next } else { $typeNode.SelectSingleNode('v8:NumberQualifiers', $nsm) }
if ($nq) {
$dn = $nq.SelectSingleNode('v8:Digits', $nsm); if ($dn) { $d = $dn.InnerText }
$fn = $nq.SelectSingleNode('v8:FractionDigits', $nsm); if ($fn) { $f = $fn.InnerText }
$sn = $nq.SelectSingleNode('v8:AllowedSign', $nsm); if ($sn -and $sn.InnerText -eq 'Nonnegative') { $sign = ',nonneg' }
}
$parts += "Number($d,$f$sign)"; break
}
'(^|:)dateTime$' {
$fr = 'DateTime'
$dq = if ($next -and $next.LocalName -eq 'DateQualifiers') { $next } else { $typeNode.SelectSingleNode('v8:DateQualifiers', $nsm) }
if ($dq) { $dn = $dq.SelectSingleNode('v8:DateFractions', $nsm); if ($dn) { $fr = $dn.InnerText } }
$parts += $fr; break # Date | DateTime
}
'(^|:)base64Binary$' { $parts += 'ValueStorage'; break }
default { $parts += (Strip-NsPrefix $raw) } # cfg:CatalogRef.X → CatalogRef.X
}
} elseif ($ln -eq 'TypeSet') {
$parts += (Strip-NsPrefix $c.InnerText.Trim()) # cfg:DefinedType.X → DefinedType.X
}
}
return ($parts -join ' + ')
}
# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
# string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно).
function Convert-ChScalarNode {
param($vN)
$xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
$txt = $vN.InnerText
if ($xt -match 'boolean$') { return ($txt -eq 'true') }
if ($xt -match 'decimal$') {
if ($txt -match '^-?\d+$') { return [int]$txt }
return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture)
}
return $txt
}
# app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value).
function Get-ChoiceParamValue {
param($valNode)
$xt = $valNode.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
if ($xt -match 'FixedArray$') {
$arr = [System.Collections.ArrayList]@()
foreach ($sub in @($valNode.SelectNodes('v8:Value', $nsm))) { [void]$arr.Add((Convert-ChScalarNode $sub)) }
return $arr
}
return Convert-ChScalarNode $valNode
}
# <ChoiceParameterLinks> → [{name,dataPath,valueChange?}] | строки "name=dataPath". $tag: 'md:...' (реквизит) | 'xr:...' (станд.).
function Parse-ChoiceParameterLinks {
param($parent, [string]$tag)
$node = $parent.SelectSingleNode($tag, $nsm)
if (-not $node) { return $null }
$links = @($node.SelectNodes('xr:Link', $nsm))
if ($links.Count -eq 0) { return $null }
$arr = [System.Collections.ArrayList]@()
foreach ($lk in $links) {
$lName = $lk.SelectSingleNode('xr:Name', $nsm).InnerText
$lDp = $lk.SelectSingleNode('xr:DataPath', $nsm).InnerText
$vcN = $lk.SelectSingleNode('xr:ValueChange', $nsm); $vcv = if ($vcN) { $vcN.InnerText } else { 'Clear' }
if ($vcv -eq 'Clear') { [void]$arr.Add("$lName=$lDp") }
else { [void]$arr.Add([ordered]@{ name = $lName; dataPath = $lDp; valueChange = $vcv }) }
}
return $arr
}
# <ChoiceParameters> → [{name,value?}]. $tag: 'md:...' | 'xr:...'.
function Parse-ChoiceParameters {
param($parent, [string]$tag)
$node = $parent.SelectSingleNode($tag, $nsm)
if (-not $node) { return $null }
$items = @($node.SelectNodes('app:item', $nsm))
if ($items.Count -eq 0) { return $null }
$arr = [System.Collections.ArrayList]@()
foreach ($it in $items) {
$pName = $it.GetAttribute('name')
$valN = $it.SelectSingleNode('app:value', $nsm)
$nilAttr = if ($valN) { $valN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') } else { '' }
if (-not $valN -or $nilAttr -eq 'true') { [void]$arr.Add([ordered]@{ name = $pName }) }
else { $o = [ordered]@{ name = $pName }; $o['value'] = Get-ChoiceParamValue $valN; [void]$arr.Add($o) }
}
return $arr
}
# --- Реквизит → DSL: shorthand-строка "Имя: Тип | флаги" ЛИБО object-форма при кастомном синониме.
# (Синоним ≠ авто → object {name, type, synonym, [flags]}; иначе компактный shorthand.) ---
function Attr-ToDsl {
param($attrNode)
$ap = $attrNode.SelectSingleNode('md:Properties', $nsm)
$nm = ($ap.SelectSingleNode('md:Name', $nsm)).InnerText
$ts = Get-TypeShorthand ($ap.SelectSingleNode('md:Type', $nsm))
$flags = @()
$fc = $ap.SelectSingleNode('md:FillChecking', $nsm); if ($fc -and $fc.InnerText -eq 'ShowError') { $flags += 'req' }
$ix = $ap.SelectSingleNode('md:Indexing', $nsm)
if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } }
$ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' }
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
$synNode = $ap.SelectSingleNode('md:Synonym', $nsm)
$synVal = Get-MLValue $synNode
$synCustom = $false
# Пустой <Synonym/> (узел есть, значения нет) ≠ авто-синоним из имени → явный пустой (synonym:"").
# У Catalog-реквизитов не встречается (0/4018), у части ExchangePlan — да.
$synEmpty = ($synNode -and $null -eq $synVal)
if ($synVal -is [string]) { if ($synVal -ne (Split-CamelWords $nm)) { $synCustom = $true } }
elseif ($null -ne $synVal) { $synCustom = $true } # {ru,en} = всегда кастом
$ttVal = Get-MLValue ($ap.SelectSingleNode('md:ToolTip', $nsm))
# Extra-свойства реквизита (omit-on-default). Наличие любого → object-форма.
# $en(tag) → InnerText узла или $null.
$en = { param($tag) $n = $ap.SelectSingleNode("md:$tag", $nsm); if ($n) { $n.InnerText } else { $null } }
$extra = [ordered]@{}
$v = & $en 'Comment'; if ($v) { $extra['comment'] = $v }
$v = & $en 'FullTextSearch'; if ($v -and $v -ne 'Use') { $extra['fullTextSearch'] = $v }
$v = & $en 'FillFromFillingValue'; if ($v -eq 'true') { $extra['fillFromFillingValue'] = $true }
$v = & $en 'CreateOnInput'; if ($v -and $v -ne 'Auto') { $extra['createOnInput'] = $v }
$v = & $en 'QuickChoice'; if ($v -and $v -ne 'Auto') { $extra['quickChoice'] = $v }
$v = & $en 'DataHistory'; if ($v -and $v -ne 'Use') { $extra['dataHistory'] = $v }
$v = & $en 'Use'; if ($v -and $v -ne 'ForItem') { $extra['use'] = $v }
$v = & $en 'PasswordMode'; if ($v -eq 'true') { $extra['passwordMode'] = $true }
$v = & $en 'Mask'; if ($v) { $extra['mask'] = $v }
$v = & $en 'ChoiceHistoryOnInput'; if ($v -and $v -ne 'Auto') { $extra['choiceHistoryOnInput'] = $v }
$v = & $en 'FillChecking'; if ($v -eq 'ShowWarning') { $extra['fillChecking'] = 'ShowWarning' }
$v = & $en 'ExtendedEdit'; if ($v -eq 'true') { $extra['extendedEdit'] = $true }
# MinValue/MaxValue — граница диапазона (omit при nil). Тип сохраняем: xs:string→строка, xs:decimal→число.
foreach ($mm in @('MinValue','MaxValue')) {
$mn = $ap.SelectSingleNode("md:$mm", $nsm)
if (-not $mn) { continue }
if ($mn.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true') { continue }
$key = if ($mm -eq 'MinValue') { 'minValue' } else { 'maxValue' }
$xt = $mn.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
$txt = $mn.InnerText
if ($xt -match 'decimal|int|double|float') {
if ($txt -match '^-?\d+$') { $extra[$key] = [long]$txt } else { $extra[$key] = [double]$txt }
} else { $extra[$key] = $txt }
}
$fmtV = Get-MLValue ($ap.SelectSingleNode('md:Format', $nsm)); if ($null -ne $fmtV) { $extra['format'] = $fmtV }
$efmtV = Get-MLValue ($ap.SelectSingleNode('md:EditFormat', $nsm)); if ($null -ne $efmtV) { $extra['editFormat'] = $efmtV }
# FillValue (значение заполнения). Форма по умолчанию зависит от типа реквизита: String→typed-empty,
# Number→zero, всё остальное→nil. Эмитим `fillValue` только при отклонении от дефолта (§4.2 spec).
# nil у String/Number → JSON null (nil-override); реальное значение → строка/bool/число/DTR-путь verbatim.
$fvNode = $ap.SelectSingleNode('md:FillValue', $nsm)
if ($fvNode) {
$fcat = 'Other'
if ($ts -match '\+') { $fcat = 'Other' }
elseif ($ts -match '^Boolean') { $fcat = 'Boolean' }
elseif ($ts -match '^String') { $fcat = 'String' }
elseif ($ts -match '^Number') { $fcat = 'Number' }
elseif ($ts -match '^(Date|DateTime)') { $fcat = 'Date' }
$nilAttr = $fvNode.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance')
$xsiT = $fvNode.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
$fvText = $fvNode.InnerText
if ($nilAttr -eq 'true') {
if ($fcat -eq 'String' -or $fcat -eq 'Number') { $extra['fillValue'] = $null } # nil-override
# иначе nil — это дефолт → пропускаем
} elseif ($xsiT -match 'boolean$') {
$extra['fillValue'] = ($fvText -eq 'true')
} elseif ($xsiT -match 'decimal$') {
if (-not ($fcat -eq 'Number' -and ($fvText -eq '0' -or $fvText -eq ''))) { $extra['fillValue'] = $fvText }
} elseif ($xsiT -match 'string$') {
if (-not ($fcat -eq 'String' -and $fvText -eq '')) { $extra['fillValue'] = $fvText }
} elseif ($xsiT -match 'dateTime$') {
$extra['fillValue'] = $fvText
} elseif ($xsiT -match 'DesignTimeRef$') {
$extra['fillValue'] = $fvText
}
}
# LinkByType (связь по типу): DataPath + LinkItem. Пусто → пропускаем. linkItem=0 → компактно строкой.
$lbtNode = $ap.SelectSingleNode('md:LinkByType', $nsm)
if ($lbtNode) {
$dpN = $lbtNode.SelectSingleNode('xr:DataPath', $nsm)
if ($dpN -and $dpN.InnerText) {
$liN = $lbtNode.SelectSingleNode('xr:LinkItem', $nsm)
$li = if ($liN -and $liN.InnerText) { [int]$liN.InnerText } else { 0 }
if ($li -eq 0) { $extra['linkByType'] = $dpN.InnerText }
else { $extra['linkByType'] = [ordered]@{ dataPath = $dpN.InnerText; linkItem = $li } }
}
}
$cplArr = Parse-ChoiceParameterLinks $ap 'md:ChoiceParameterLinks'; if ($null -ne $cplArr) { $extra['choiceParameterLinks'] = $cplArr }
$cpArr = Parse-ChoiceParameters $ap 'md:ChoiceParameters'; if ($null -ne $cpArr) { $extra['choiceParameters'] = $cpArr }
if ($synCustom -or $synEmpty -or ($null -ne $ttVal) -or $extra.Count -gt 0) {
$o = [ordered]@{ name = $nm }
if ($ts) { $o['type'] = $ts }
if ($synCustom) { $o['synonym'] = $synVal }
elseif ($synEmpty) { $o['synonym'] = '' }
if ($null -ne $ttVal) { $o['tooltip'] = $ttVal }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
if ($flags.Count -gt 0) { $o['flags'] = [System.Collections.ArrayList]@($flags) }
return $o
}
$head = if ($ts) { "${nm}: $ts" } else { $nm }
if ($flags.Count -gt 0) { return "$head | " + ($flags -join ', ') }
return $head
}
# === Сборка DSL ===
$dsl = [ordered]@{ type = $objType; name = $objName }
# Синоним объекта: строка ru-only ИЛИ {ru,en} (мультиязычно). Кастом → эмитим.
$synVal = Get-MLValue ($props.SelectSingleNode('md:Synonym', $nsm))
if ($synVal -is [string]) { if ($synVal -ne (Split-CamelWords $objName)) { $dsl['synonym'] = $synVal } }
elseif ($null -ne $synVal) { $dsl['synonym'] = $synVal }
$cmt = P 'Comment'; if ($cmt) { $dsl['comment'] = $cmt }
# Свойства Catalog (omit-on-default). Порядок ключей — как удобно DSL.
function Add-BoolProp { param([string]$key, [string]$tag, [bool]$default) $v = P $tag; if ($null -ne $v) { $b = ($v -eq 'true'); if ($b -ne $default) { $dsl[$key] = $b } } }
function Add-EnumProp { param([string]$key, [string]$tag, [string]$default) $v = P $tag; if ($null -ne $v -and $v -ne '' -and $v -ne $default) { $dsl[$key] = $v } }
function Add-IntProp { param([string]$key, [string]$tag, [int]$default) $v = P $tag; if ($null -ne $v -and $v -ne '') { $iv = [int]$v; if ($iv -ne $default) { $dsl[$key] = $iv } } }
Add-BoolProp 'hierarchical' 'Hierarchical' $false
Add-EnumProp 'hierarchyType' 'HierarchyType' 'HierarchyFoldersAndItems'
Add-BoolProp 'limitLevelCount' 'LimitLevelCount' $false
Add-IntProp 'levelCount' 'LevelCount' 2
Add-BoolProp 'foldersOnTop' 'FoldersOnTop' $true
# owners
$ownersNode = $props.SelectSingleNode('md:Owners', $nsm)
if ($ownersNode) {
$items = @($ownersNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
if ($items.Count -gt 0) { $dsl['owners'] = [System.Collections.ArrayList]@($items | ForEach-Object { if ($_ -match '^Catalog\.') { ($_ -split '\.', 2)[1] } else { $_ } }) }
}
Add-EnumProp 'subordinationUse' 'SubordinationUse' 'ToItems'
# Тип-зависимые дефолты (компилятор задаёт их по типу — декомпилятор обязан зеркалить, иначе omit ≠ значению).
$descrLenDef = switch ($objType) { 'ExchangePlan' { 150 } 'ChartOfCharacteristicTypes' { 100 } default { 25 } }
$createInpDef = if ($objType -eq 'Catalog') { 'Use' } else { 'DontUse' }
$dataLockDef = if ($objType -in @('Catalog', 'ChartOfAccounts')) { 'Automatic' } else { 'Managed' }
$codeSeriesDef = switch ($objType) { 'ChartOfCharacteristicTypes' { 'WholeCharacteristicKind' } 'ChartOfAccounts' { 'WholeChartOfAccounts' } default { 'WholeCatalog' } }
$checkUniqueDef = ($objType -in @('ChartOfCharacteristicTypes', 'ChartOfAccounts')) # ПВХ/ПС дефолт true, Catalog false
$defPresDef = if ($objType -eq 'ChartOfAccounts') { 'AsCode' } else { 'AsDescription' } # ПС по умолчанию AsCode
Add-IntProp 'codeLength' 'CodeLength' 9
Add-IntProp 'descriptionLength' 'DescriptionLength' $descrLenDef
Add-EnumProp 'codeType' 'CodeType' 'String'
Add-EnumProp 'codeAllowedLength' 'CodeAllowedLength' 'Variable'
Add-BoolProp 'autonumbering' 'Autonumbering' $true
Add-BoolProp 'checkUnique' 'CheckUnique' $checkUniqueDef
Add-EnumProp 'codeSeries' 'CodeSeries' $codeSeriesDef
Add-EnumProp 'defaultPresentation' 'DefaultPresentation' $defPresDef
Add-BoolProp 'quickChoice' 'QuickChoice' $false
Add-EnumProp 'choiceMode' 'ChoiceMode' 'BothWays'
Add-EnumProp 'dataLockControlMode' 'DataLockControlMode' $dataLockDef
Add-EnumProp 'fullTextSearch' 'FullTextSearch' 'Use'
Add-BoolProp 'useStandardCommands' 'UseStandardCommands' $true
Add-EnumProp 'createOnInput' 'CreateOnInput' $createInpDef
Add-EnumProp 'editType' 'EditType' 'InDialog'
Add-BoolProp 'includeHelpInContents' 'IncludeHelpInContents' $false
Add-EnumProp 'choiceHistoryOnInput' 'ChoiceHistoryOnInput' 'Auto'
Add-EnumProp 'predefinedDataUpdate' 'PredefinedDataUpdate' 'Auto'
Add-EnumProp 'searchStringModeOnInputByString' 'SearchStringModeOnInputByString' 'Begin'
# ExchangePlan-специфичные свойства (у Catalog этих тегов нет → блок не трогает его).
if ($objType -eq 'ExchangePlan') {
Add-BoolProp 'distributedInfoBase' 'DistributedInfoBase' $false
Add-BoolProp 'includeConfigurationExtensions' 'IncludeConfigurationExtensions' $false
Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# ChartOfCharacteristicTypes-специфичные свойства.
if ($objType -eq 'ChartOfCharacteristicTypes') {
Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
$cev = P 'CharacteristicExtValues'; if ($cev) { $dsl['characteristicExtValues'] = $cev }
# Type — тип значения характеристики; valueType при отличии от дефолта (Boolean+String(100)+Number(15,2)+DateTime).
$vtNode = $props.SelectSingleNode('md:Type', $nsm)
if ($vtNode) {
$vtStr = Get-TypeShorthand $vtNode
if ($vtStr -and $vtStr -ne 'Boolean + String(100) + Number(15,2) + DateTime') { $dsl['valueType'] = $vtStr }
}
}
# ChartOfAccounts-специфичные свойства (у Catalog этих тегов нет → блок его не трогает).
if ($objType -eq 'ChartOfAccounts') {
$edt = P 'ExtDimensionTypes'; if ($edt) { $dsl['extDimensionTypes'] = $edt }
Add-IntProp 'maxExtDimensionCount' 'MaxExtDimensionCount' 3
$cm = P 'CodeMask'; if ($cm) { $dsl['codeMask'] = $cm }
Add-BoolProp 'autoOrderByCode' 'AutoOrderByCode' $true
Add-IntProp 'orderLength' 'OrderLength' 9
Add-EnumProp 'dataHistory' 'DataHistory' 'DontUse'
Add-BoolProp 'updateDataHistoryImmediatelyAfterWrite' 'UpdateDataHistoryImmediatelyAfterWrite' $false
Add-BoolProp 'executeAfterWriteDataHistoryVersionProcessing' 'ExecuteAfterWriteDataHistoryVersionProcessing' $false
}
# Короткая форма поля: <Type>.<Name>.StandardAttribute.X / .Attribute.X → StandardAttribute.X / Attribute.X
# (Expand-DataPath компилятора разворачивает частичную форму обратно — dogfood резолвера).
function Short-Field { param([string]$full) if ($full -match '\.(StandardAttribute|Attribute)\.(.+)$') { return "$($Matches[1]).$($Matches[2])" } return $full }
# InputByString — эмитим только при отличии от выведенного дефолта [Descr при D>0]+[Code при C>0].
$ibNode = $props.SelectSingleNode('md:InputByString', $nsm)
if ($ibNode) {
$ibActual = @($ibNode.SelectNodes('xr:Field', $nsm) | ForEach-Object { $_.InnerText })
$clv = P 'CodeLength'; $dlv = P 'DescriptionLength'
$cl = if ($clv -and $clv -ne '') { [int]$clv } else { 9 }
$dl = if ($dlv -and $dlv -ne '') { [int]$dlv } else { 25 }
$ibDef = @()
if ($dl -gt 0) { $ibDef += "StandardAttribute.Description" }
if ($cl -gt 0) { $ibDef += "StandardAttribute.Code" }
$ibShort = @($ibActual | ForEach-Object { Short-Field $_ })
$same = ($ibShort.Count -eq $ibDef.Count)
if ($same) { for ($k = 0; $k -lt $ibShort.Count; $k++) { if ($ibShort[$k] -ne $ibDef[$k]) { $same = $false; break } } }
if (-not $same) { $dsl['inputByString'] = [System.Collections.ArrayList]@($ibShort) }
}
# BasedOn — «ввод на основании», список MDObjectRef (omit-on-empty).
$boNode = $props.SelectSingleNode('md:BasedOn', $nsm)
if ($boNode) {
$boItems = @($boNode.SelectNodes('xr:Item', $nsm) | ForEach-Object { $_.InnerText })
if ($boItems.Count -gt 0) { $dsl['basedOn'] = [System.Collections.ArrayList]@($boItems) }
}
# DataLockFields — поля блокировки данных (omit-on-empty).
$dlfNode = $props.SelectSingleNode('md:DataLockFields', $nsm)
if ($dlfNode) {
$dlfFields = @($dlfNode.SelectNodes('xr:Field', $nsm) | ForEach-Object { Short-Field $_.InnerText })
if ($dlfFields.Count -gt 0) { $dsl['dataLockFields'] = [System.Collections.ArrayList]@($dlfFields) }
}
# Формы по умолчанию (компилятор пишет пусто → omit-on-empty; значение = ссылка на форму verbatim).
function Add-FormRef { param([string]$key, [string]$tag) $v = P $tag; if ($v) { $dsl[$key] = $v } }
Add-FormRef 'defaultObjectForm' 'DefaultObjectForm'
Add-FormRef 'defaultFolderForm' 'DefaultFolderForm'
Add-FormRef 'defaultListForm' 'DefaultListForm'
Add-FormRef 'defaultChoiceForm' 'DefaultChoiceForm'
Add-FormRef 'defaultFolderChoiceForm' 'DefaultFolderChoiceForm'
Add-FormRef 'auxiliaryObjectForm' 'AuxiliaryObjectForm'
Add-FormRef 'auxiliaryFolderForm' 'AuxiliaryFolderForm'
Add-FormRef 'auxiliaryListForm' 'AuxiliaryListForm'
Add-FormRef 'auxiliaryChoiceForm' 'AuxiliaryChoiceForm'
Add-FormRef 'auxiliaryFolderChoiceForm' 'AuxiliaryFolderChoiceForm'
# Презентации (ML, компилятор пишет пусто → omit-on-empty).
foreach ($pp in @(
@('ObjectPresentation','objectPresentation'), @('ExtendedObjectPresentation','extendedObjectPresentation'),
@('ListPresentation','listPresentation'), @('ExtendedListPresentation','extendedListPresentation'),
@('Explanation','explanation'))) {
$pv = Get-MLValue ($props.SelectSingleNode("md:$($pp[0])", $nsm))
if ($null -ne $pv) { $dsl[$pp[1]] = $pv }
}
# --- Characteristics (привязка ПВХ). Короткая форма: поля bare/partial, filterValue без каталога, from полный.
function Shorten-CharField { param([string]$full, [string]$from)
if ($full.StartsWith("$from.")) {
$rest = $full.Substring($from.Length + 1)
if ($rest -match '^StandardAttribute\.(Ref|Parent|Owner)$') { return $Matches[1] } # ссылочные станд. → голое
if ($rest -match '^Attribute\.(.+)$') { return $Matches[1] } # кастом → голое
return $rest # прочие StandardAttribute.X / Dimension.X / Resource.X → частичное (безопасно)
}
return $full
}
$charsNode = $props.SelectSingleNode('md:Characteristics', $nsm)
if ($charsNode) {
$chList = @($charsNode.SelectNodes('xr:Characteristic', $nsm))
if ($chList.Count -gt 0) {
$chArr = [System.Collections.ArrayList]@()
foreach ($ch in $chList) {
$ct = $ch.SelectSingleNode('xr:CharacteristicTypes', $nsm)
$cv = $ch.SelectSingleNode('xr:CharacteristicValues', $nsm)
$tFrom = $ct.GetAttribute('from'); $vFrom = $cv.GetAttribute('from')
$gt = { param($n, $node) $x = $node.SelectSingleNode("xr:$n", $nsm); if ($x) { $x.InnerText } else { "" } }
$giv = { param($n, $node) $x = $node.SelectSingleNode("xr:$n", $nsm); if ($x -and $x.InnerText -ne '') { [int]$x.InnerText } else { -1 } }
$tfvNode = $ct.SelectSingleNode('xr:TypesFilterValue', $nsm)
$tfvNil = if ($tfvNode) { $tfvNode.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') } else { '' }
$types = [ordered]@{
from = $tFrom
key = Shorten-CharField (& $gt 'KeyField' $ct) $tFrom
filterField = Shorten-CharField (& $gt 'TypesFilterField' $ct) $tFrom
filterValue = if ($tfvNil -eq 'true') { $null } else { Convert-ChScalarNode $tfvNode }
}
$dpf = & $giv 'DataPathField' $ct; if ($dpf -ne -1) { $types['dataPathField'] = $dpf }
$mvu = & $giv 'MultipleValuesUseField' $ct; if ($mvu -ne -1) { $types['multipleValuesUseField'] = $mvu }
$values = [ordered]@{
from = $vFrom
object = Shorten-CharField (& $gt 'ObjectField' $cv) $vFrom
type = Shorten-CharField (& $gt 'TypeField' $cv) $vFrom
value = Shorten-CharField (& $gt 'ValueField' $cv) $vFrom
}
$mvk = & $giv 'MultipleValuesKeyField' $cv; if ($mvk -ne -1) { $values['multipleValuesKeyField'] = $mvk }
$mvo = & $giv 'MultipleValuesOrderField' $cv; if ($mvo -ne -1) { $values['multipleValuesOrderField'] = $mvo }
[void]$chArr.Add([ordered]@{ types = $types; values = $values })
}
$dsl['characteristics'] = $chArr
}
}
# --- StandardAttributes: захватываем ОТКЛОНЕНИЯ от профиля материализованного блока (профиль компилятор
# восстановит сам). Профиль зеркалит stdAttrProfile компилятора — по типу объекта.
# Catalog: Owner{FC=ShowError,FFV=true}, Parent{FFV=true}, Description{FC=ShowError}.
# ExchangePlan: Description{FC=ShowError}, Code{FC=ShowError} (блок всегда материализован). ---
$stdProfileByType = @{
'Catalog' = @{
'Owner' = @{ fillChecking = 'ShowError'; fillFromFillingValue = $true }
'Parent' = @{ fillFromFillingValue = $true }
'Description' = @{ fillChecking = 'ShowError' }
}
'ExchangePlan' = @{
'Description' = @{ fillChecking = 'ShowError' }
'Code' = @{ fillChecking = 'ShowError' }
}
'ChartOfCharacteristicTypes' = @{
'Description' = @{ fillChecking = 'ShowError' }
'Parent' = @{ fillFromFillingValue = $true }
}
'ChartOfAccounts' = @{
'Description' = @{ fillChecking = 'ShowError' }
'Code' = @{ fillChecking = 'ShowError' }
'Parent' = @{ fillFromFillingValue = $true }
}
}
$catStdProfile = if ($stdProfileByType.ContainsKey($objType)) { $stdProfileByType[$objType] } else { @{} }
# Фикс-список стандартных реквизитов типа (зеркало standardAttributesByType компилятора) — чтобы отличать
# доп./опциональные (напр. ExchangeDate у ПланОбмена), которые эмитим по факту присутствия даже all-default.
$stdFixedByType = @{
'Catalog' = @('PredefinedDataName','Predefined','Ref','DeletionMark','IsFolder','Owner','Parent','Description','Code')
'ExchangePlan' = @('Ref','DeletionMark','Code','Description','ThisNode','SentNo','ReceivedNo')
'ChartOfCharacteristicTypes' = @('PredefinedDataName','Predefined','Ref','DeletionMark','Description','Code','Parent','ValueType')
'ChartOfAccounts' = @('PredefinedDataName','Order','OffBalance','Type','Description','Code','Parent','Predefined','DeletionMark','Ref')
}
$stdFixed = if ($stdFixedByType.ContainsKey($objType)) { $stdFixedByType[$objType] } else { @() }
# Условные типы: блок эмитим-как-триггер даже пустым (материализуется при отклонении ≥1 реквизита от schema-default;
# у ExchangePlan это почти всегда — Description/Code=ShowError; редкий all-default EP блок опускает).
$stdConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts')
$saNode = $props.SelectSingleNode('md:StandardAttributes', $nsm)
if ($saNode) {
$saMap = [ordered]@{}
foreach ($sa in @($saNode.SelectNodes('xr:StandardAttribute', $nsm))) {
$an = $sa.GetAttribute('name')
$prof = if ($catStdProfile.ContainsKey($an)) { $catStdProfile[$an] } else { @{} }
$ov = [ordered]@{}
# FillChecking (профиль или DontCheck)
$fcN = $sa.SelectSingleNode('xr:FillChecking', $nsm); $fc = if ($fcN) { $fcN.InnerText } else { 'DontCheck' }
$profFc = if ($prof.ContainsKey('fillChecking')) { $prof['fillChecking'] } else { 'DontCheck' }
if ($fc -ne $profFc) { $ov['fillChecking'] = $fc }
# FillFromFillingValue (профиль или false)
$ffvN = $sa.SelectSingleNode('xr:FillFromFillingValue', $nsm); $ffv = ($ffvN -and $ffvN.InnerText -eq 'true')
$profFfv = ($prof['fillFromFillingValue'] -eq $true)
if ($ffv -ne $profFfv) { $ov['fillFromFillingValue'] = $ffv }
# Synonym / ToolTip (профиль пуст) — строка ru | {ru,en}
$syn = Get-MLValue ($sa.SelectSingleNode('xr:Synonym', $nsm))
if ($null -ne $syn) { $ov['synonym'] = $syn }
$tt = Get-MLValue ($sa.SelectSingleNode('xr:ToolTip', $nsm))
if ($null -ne $tt) { $ov['tooltip'] = $tt }
# FullTextSearch / DataHistory (профиль = Use)
$ftsN = $sa.SelectSingleNode('xr:FullTextSearch', $nsm); if ($ftsN -and $ftsN.InnerText -ne 'Use') { $ov['fullTextSearch'] = $ftsN.InnerText }
$dhN = $sa.SelectSingleNode('xr:DataHistory', $nsm); if ($dhN -and $dhN.InnerText -ne 'Use') { $ov['dataHistory'] = $dhN.InnerText }
# FillValue (дефолт nil) — DTR-путь/строка/bool. Comment/Mask/ChoiceForm (дефолт пусто).
$fvN = $sa.SelectSingleNode('xr:FillValue', $nsm)
if ($fvN -and $fvN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -ne 'true') { $ov['fillValue'] = Convert-ChScalarNode $fvN }
$saCmt = $sa.SelectSingleNode('xr:Comment', $nsm); if ($saCmt -and $saCmt.InnerText) { $ov['comment'] = $saCmt.InnerText }
$saMsk = $sa.SelectSingleNode('xr:Mask', $nsm); if ($saMsk -and $saMsk.InnerText) { $ov['mask'] = $saMsk.InnerText }
$saCf = $sa.SelectSingleNode('xr:ChoiceForm', $nsm); if ($saCf -and $saCf.InnerText) { $ov['choiceForm'] = $saCf.InnerText }
$saCpl = Parse-ChoiceParameterLinks $sa 'xr:ChoiceParameterLinks'; if ($null -ne $saCpl) { $ov['choiceParameterLinks'] = $saCpl }
$saCp = Parse-ChoiceParameters $sa 'xr:ChoiceParameters'; if ($null -ne $saCp) { $ov['choiceParameters'] = $saCp }
# Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений.
if ($ov.Count -gt 0 -or ($stdFixed -notcontains $an)) { $saMap[$an] = $ov }
}
# Условный тип (Catalog): пустой $saMap = триггер блока. Не-условный (ExchangePlan): блок и так эмитится → пустой не пишем.
if ($saMap.Count -gt 0 -or ($stdConditionalTypes -contains $objType)) { $dsl['standardAttributes'] = $saMap }
}
# --- ChildObjects: Attributes + TabularSections ---
$childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
if ($childObjs) {
$attrs = @($childObjs.SelectNodes('md:Attribute', $nsm))
if ($attrs.Count -gt 0) {
$arr = [System.Collections.ArrayList]@()
foreach ($a in $attrs) { [void]$arr.Add((Attr-ToDsl $a)) }
$dsl['attributes'] = $arr
}
# ChartOfAccounts: признаки учёта (AccountingFlag) и признаки учёта субконто (ExtDimensionAccountingFlag) —
# структурно как реквизит, захватываем тем же Attr-ToDsl (тип Boolean уходит в короткую запись).
$acctFlagNodes = @($childObjs.SelectNodes('md:AccountingFlag', $nsm))
if ($acctFlagNodes.Count -gt 0) {
$arr = [System.Collections.ArrayList]@()
foreach ($a in $acctFlagNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
$dsl['accountingFlags'] = $arr
}
$extDimFlagNodes = @($childObjs.SelectNodes('md:ExtDimensionAccountingFlag', $nsm))
if ($extDimFlagNodes.Count -gt 0) {
$arr = [System.Collections.ArrayList]@()
foreach ($a in $extDimFlagNodes) { [void]$arr.Add((Attr-ToDsl $a)) }
$dsl['extDimensionAccountingFlags'] = $arr
}
$tsNodes = @($childObjs.SelectNodes('md:TabularSection', $nsm))
if ($tsNodes.Count -gt 0) {
$tsMap = [ordered]@{}
foreach ($ts in $tsNodes) {
$tsp = $ts.SelectSingleNode('md:Properties', $nsm)
$tsName = ($tsp.SelectSingleNode('md:Name', $nsm)).InnerText
$tco = $ts.SelectSingleNode('md:ChildObjects', $nsm)
$cols = [System.Collections.ArrayList]@()
if ($tco) { foreach ($ca in @($tco.SelectNodes('md:Attribute', $nsm))) { [void]$cols.Add((Attr-ToDsl $ca)) } }
# Синоним/подсказка/комментарий ТЧ. Кастом → объектная форма {synonym?, tooltip?, comment?, attributes}.
$tsSyn = Get-MLValue ($tsp.SelectSingleNode('md:Synonym', $nsm))
$tsSynCustom = $false
if ($tsSyn -is [string]) { if ($tsSyn -ne (Split-CamelWords $tsName)) { $tsSynCustom = $true } }
elseif ($null -ne $tsSyn) { $tsSynCustom = $true }
$tsTt = Get-MLValue ($tsp.SelectSingleNode('md:ToolTip', $nsm))
$tsCmtN = $tsp.SelectSingleNode('md:Comment', $nsm); $tsCmt = if ($tsCmtN) { $tsCmtN.InnerText } else { '' }
# Кастомизация стандартного реквизита LineNumber (НомерСтроки) — omit-on-default по каждому свойству.
$lnObj = [ordered]@{}
$lnNode = $tsp.SelectSingleNode("md:StandardAttributes/xr:StandardAttribute[@name='LineNumber']", $nsm)
if ($lnNode) {
$lnSyn = Get-MLValue ($lnNode.SelectSingleNode('xr:Synonym', $nsm)); if ($null -ne $lnSyn) { $lnObj['synonym'] = $lnSyn }
$lnCmtN = $lnNode.SelectSingleNode('xr:Comment', $nsm); if ($lnCmtN -and $lnCmtN.InnerText) { $lnObj['comment'] = $lnCmtN.InnerText }
$lnFtsN = $lnNode.SelectSingleNode('xr:FullTextSearch', $nsm); if ($lnFtsN -and $lnFtsN.InnerText -ne 'Use') { $lnObj['fullTextSearch'] = $lnFtsN.InnerText }
$lnTt = Get-MLValue ($lnNode.SelectSingleNode('xr:ToolTip', $nsm)); if ($null -ne $lnTt) { $lnObj['tooltip'] = $lnTt }
$lnFmt = Get-MLValue ($lnNode.SelectSingleNode('xr:Format', $nsm)); if ($null -ne $lnFmt) { $lnObj['format'] = $lnFmt }
$lnEfmt = Get-MLValue ($lnNode.SelectSingleNode('xr:EditFormat', $nsm)); if ($null -ne $lnEfmt) { $lnObj['editFormat'] = $lnEfmt }
$lnChiN = $lnNode.SelectSingleNode('xr:ChoiceHistoryOnInput', $nsm); if ($lnChiN -and $lnChiN.InnerText -ne 'Auto') { $lnObj['choiceHistoryOnInput'] = $lnChiN.InnerText }
}
if ($tsSynCustom -or ($null -ne $tsTt) -or $tsCmt -or $lnObj.Count -gt 0) {
$to = [ordered]@{}
if ($tsSynCustom) { $to['synonym'] = $tsSyn }
if ($null -ne $tsTt) { $to['tooltip'] = $tsTt }
if ($tsCmt) { $to['comment'] = $tsCmt }
if ($lnObj.Count -gt 0) { $to['lineNumber'] = $lnObj }
$to['attributes'] = $cols
$tsMap[$tsName] = $to
} else {
$tsMap[$tsName] = $cols
}
}
$dsl['tabularSections'] = $tsMap
}
# --- Commands (полноблочные <Command> в ChildObjects) → DSL commands (map имя→объект, omit-on-default).
# Тела модулей команд (CommandModule.bsl) — вне скоупа (как ObjectModule). ---
$cmdNodes = @($childObjs.SelectNodes('md:Command', $nsm))
if ($cmdNodes.Count -gt 0) {
$cmdMap = [ordered]@{}
foreach ($cm in $cmdNodes) {
$cp = $cm.SelectSingleNode('md:Properties', $nsm)
$cn = ($cp.SelectSingleNode('md:Name', $nsm)).InnerText
$o = [ordered]@{}
$syn = Get-MLValue ($cp.SelectSingleNode('md:Synonym', $nsm))
if ($syn -is [string]) { if ($syn -ne (Split-CamelWords $cn)) { $o['synonym'] = $syn } }
elseif ($null -ne $syn) { $o['synonym'] = $syn }
$cmtN = $cp.SelectSingleNode('md:Comment', $nsm); if ($cmtN -and $cmtN.InnerText) { $o['comment'] = $cmtN.InnerText }
$grpN = $cp.SelectSingleNode('md:Group', $nsm); if ($grpN -and $grpN.InnerText) { $o['group'] = $grpN.InnerText }
$cpt = Get-TypeShorthand ($cp.SelectSingleNode('md:CommandParameterType', $nsm)); if ($cpt) { $o['commandParameterType'] = $cpt }
$pumN = $cp.SelectSingleNode('md:ParameterUseMode', $nsm); if ($pumN -and $pumN.InnerText -ne 'Single') { $o['parameterUseMode'] = $pumN.InnerText }
$mdN = $cp.SelectSingleNode('md:ModifiesData', $nsm); if ($mdN -and $mdN.InnerText -eq 'true') { $o['modifiesData'] = $true }
$repN = $cp.SelectSingleNode('md:Representation', $nsm); if ($repN -and $repN.InnerText -ne 'Auto') { $o['representation'] = $repN.InnerText }
$ctt = Get-MLValue ($cp.SelectSingleNode('md:ToolTip', $nsm)); if ($null -ne $ctt) { $o['tooltip'] = $ctt }
# <Picture> — структурный блок (зеркало form-decompile Set-CommandPicture). Дефолт LoadTransparent=true:
# скаляр `picture` + sibling `loadTransparent:false` при отклонении; объект {src,loadTransparent?,transparentPixel} при TransparentPixel.
$refN = $cp.SelectSingleNode('md:Picture/xr:Ref', $nsm)
$absN = $cp.SelectSingleNode('md:Picture/xr:Abs', $nsm)
if ($refN -or $absN) {
$psrc = if ($refN) { $refN.InnerText } else { "abs:$($absN.InnerText)" }
$ltN = $cp.SelectSingleNode('md:Picture/xr:LoadTransparent', $nsm)
$ltFalse = ($ltN -and $ltN.InnerText -eq 'false')
$tpxN = $cp.SelectSingleNode('md:Picture/xr:TransparentPixel', $nsm)
if ($tpxN) {
$po = [ordered]@{ src = $psrc }
if ($ltFalse) { $po['loadTransparent'] = $false }
$po['transparentPixel'] = [ordered]@{ x = [int]$tpxN.GetAttribute('x'); y = [int]$tpxN.GetAttribute('y') }
$o['picture'] = $po
} else {
$o['picture'] = $psrc
if ($ltFalse) { $o['loadTransparent'] = $false }
}
}
$scN = $cp.SelectSingleNode('md:Shortcut', $nsm); if ($scN -and $scN.InnerText) { $o['shortcut'] = $scN.InnerText }
$osuN = $cp.SelectSingleNode('md:OnMainServerUnavalableBehavior', $nsm); if ($osuN -and $osuN.InnerText -ne 'Auto') { $o['onMainServerUnavalableBehavior'] = $osuN.InnerText }
$cmdMap[$cn] = $o
}
$dsl['commands'] = $cmdMap
}
}
# --- Предопределённые (соседний Ext/Predefined.xml) → DSL predefined.
# Плоский элемент → строка "(Код) Имя [Наименование]" (Наименование: ==авто → опустить; '' → []; иначе [текст]).
# Группа/иерархия → object {name, [code], [description], isFolder, childItems}. codeType — из свойства каталога. ---
$objDir = Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path
$predefPath = Join-Path (Join-Path (Join-Path $objDir $objName) 'Ext') 'Predefined.xml'
if (Test-Path -LiteralPath $predefPath) {
$pdoc = New-Object System.Xml.XmlDocument
$pdoc.Load($predefPath)
function PredefItem-ToDsl {
param($itemEl)
$name = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']")
$code = if ($codeEl -and $codeEl.InnerText) { $codeEl.InnerText } else { '' }
$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']")
$desc = if ($descEl) { $descEl.InnerText } else { '' }
$folderEl = $itemEl.SelectSingleNode("*[local-name()='IsFolder']")
$isFolder = ($folderEl -and $folderEl.InnerText -eq 'true')
$childContainer = $itemEl.SelectSingleNode("*[local-name()='ChildItems']")
# ВАЖНО: обернуть весь if в @(), иначе PS распаковывает одноэлементный @(...) из if-блока
# обратно в узел → $kids.Count = $null → папки с ОДНИМ ребёнком теряют его.
$kids = @(if ($childContainer) { $childContainer.SelectNodes("*[local-name()='Item']") } else { @() })
# Type — тип значения предопределённой характеристики (ПВХ). Наличие → object-форма (в строку не влезает).
# Наличие узла <Type> (даже пустого <Type/>) → object-форма с ключом type ('' для пустого); нет узла (Catalog) → без.
$typeEl = $itemEl.SelectSingleNode("*[local-name()='Type']")
$typeStr = if ($typeEl) { Get-TypeShorthand $typeEl } else { $null }
$auto = Split-CamelWords $name
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
# Пустой <Type/> в короткую не влезает (нужен явный маркер) → object-форма с type:''.
if (-not $isFolder -and $kids.Count -eq 0 -and ($null -eq $typeStr -or $typeStr -ne '')) {
$s = if ($code) { "($code) $name" } else { $name }
if ($desc -eq '') { $s = "$s []" }
elseif ($desc -ne $auto) { $s = "$s [$desc]" }
if ($typeStr) { $s = "${s}: $typeStr" }
return $s
}
# Группа/иерархия/с типом → объект.
$o = [ordered]@{ name = $name }
if ($code) { $o['code'] = $code }
if ($desc -eq '') { $o['description'] = '' }
elseif ($desc -ne $auto) { $o['description'] = $desc }
if ($null -ne $typeStr) { $o['type'] = $typeStr }
if ($isFolder) { $o['isFolder'] = $true }
if ($kids.Count -gt 0) {
$sub = [System.Collections.ArrayList]@()
foreach ($k in $kids) { [void]$sub.Add((PredefItem-ToDsl $k)) }
$o['childItems'] = $sub
}
return $o
}
# Предопределённые СЧЕТА Плана счетов — отдельная грамматика (AccountType/OffBalance/Order/AccountingFlags/
# ExtDimensionTypes). Флаги: захватываем только TRUE (компилятор развернёт по def-порядку признаков плана).
function PredefAccount-ToDsl {
param($itemEl)
$name = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']")
$code = if ($codeEl -and $codeEl.InnerText) { $codeEl.InnerText } else { '' }
$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']")
$desc = if ($descEl) { $descEl.InnerText } else { '' }
$atEl = $itemEl.SelectSingleNode("*[local-name()='AccountType']")
$acctType = if ($atEl) { $atEl.InnerText } else { 'ActivePassive' }
$offEl = $itemEl.SelectSingleNode("*[local-name()='OffBalance']")
$off = ($offEl -and $offEl.InnerText -eq 'true')
$ordEl = $itemEl.SelectSingleNode("*[local-name()='Order']")
$order = if ($ordEl) { $ordEl.InnerText } else { '' }
$auto = Split-CamelWords $name
# TRUE-флаги (leaf после последней точки в ref).
$trueFlags = @()
foreach ($fl in @($itemEl.SelectNodes("*[local-name()='AccountingFlags']/*[local-name()='Flag']"))) {
if ($fl.InnerText -eq 'true') { $r = $fl.GetAttribute('ref'); $trueFlags += ($r -split '\.')[-1] }
}
# ExtDimensionTypes → subconto. Короткая запись "Тип | Признак1, Признак2": срезаем префикс ПВХ видов субконто
# плана (extDimensionTypes); «Только обороты» (Turnover=true) — предопределённый признак-токен `Turnover` первым
# в списке (наравне с добавленными). Захватываем только TRUE-признаки (компилятор развернёт по def-порядку).
$edtPfx = if ($dsl['extDimensionTypes']) { "$($dsl['extDimensionTypes'])." } else { $null }
$subconto = [System.Collections.ArrayList]@()
foreach ($edt in @($itemEl.SelectNodes("*[local-name()='ExtDimensionTypes']/*[local-name()='ExtDimensionType']"))) {
$scT = $edt.GetAttribute('name')
if ($edtPfx -and $scT.StartsWith($edtPfx)) { $scT = $scT.Substring($edtPfx.Length) }
$tEl = $edt.SelectSingleNode("*[local-name()='Turnover']")
$scFlagsOut = @()
if ($tEl -and $tEl.InnerText -eq 'true') { $scFlagsOut += 'Turnover' }
foreach ($fl in @($edt.SelectNodes("*[local-name()='AccountingFlags']/*[local-name()='Flag']"))) {
if ($fl.InnerText -eq 'true') { $r = $fl.GetAttribute('ref'); $scFlagsOut += ($r -split '\.')[-1] }
}
if ($scFlagsOut.Count -gt 0) { [void]$subconto.Add("$scT | " + ($scFlagsOut -join ', ')) }
else { [void]$subconto.Add($scT) }
}
$childContainer = $itemEl.SelectSingleNode("*[local-name()='ChildItems']")
$kids = @(if ($childContainer) { $childContainer.SelectNodes("*[local-name()='Item']") } else { @() })
$o = [ordered]@{ name = $name }
if ($code) { $o['code'] = $code }
# -cne (регистрочувствительно!): PS `-ne` регистронезависим → «…ОС» == «…ос» и description ошибочно
# опускался, а компилятор регенерил lowercase (Split-CamelCase). Хвостовые аббревиатуры (ОС/НМА) теряли регистр.
if ($desc -cne $auto) { $o['description'] = $desc }
$o['accountType'] = $acctType
if ($off) { $o['offBalance'] = $true }
$o['order'] = $order
if ($trueFlags.Count -gt 0) { $o['flags'] = [System.Collections.ArrayList]@($trueFlags) }
if ($subconto.Count -gt 0) { $o['subconto'] = $subconto }
if ($kids.Count -gt 0) {
$sub = [System.Collections.ArrayList]@()
foreach ($k in $kids) { [void]$sub.Add((PredefAccount-ToDsl $k)) }
$o['childItems'] = $sub
}
return $o
}
$rootItems = [System.Collections.ArrayList]@()
if ($objType -eq 'ChartOfAccounts') {
foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((PredefAccount-ToDsl $it)) }
} else {
foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((PredefItem-ToDsl $it)) }
}
if ($rootItems.Count -gt 0) { $dsl['predefined'] = $rootItems }
}
# === Вывод ===
$json = ConvertTo-CompactJson $dsl 0
if ($OutputPath) {
[System.IO.File]::WriteAllText($OutputPath, $json, (New-Object System.Text.UTF8Encoding($false)))
} else {
[Console]::Out.WriteLine($json)
}