mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-30 08:26:55 +03:00
Раундтрип-находка (класс-1 синоним + класс-3 подсказка). Корпус: у реквизитов 21677 мультиязычных
синонимов (ru+en) и 16063 непустых подсказки (10689 мультиязычных). meta-compile писал ВСЁ ML только ru
(Emit-MLText брал строку) и хардкодил <ToolTip/> пустым → массовая потеря en + подсказок.
- meta-compile (дуал-порт): Emit-MLText/emit_mltext принимают строку (→ru) ИЛИ объект {lang:content}
(→<v8:item> на язык, в порядке ключей) через новый Emit-MLItems/emit_ml_items. Parse object-форма
реквизита пробрасывает synonym/tooltip без стрингификации; Emit-Attribute эмитит <ToolTip> из tooltip.
- meta-decompile: Get-MLValue → строка (ru-only) | {ru,en} (мультиязычно, порядок из XML); object-форма
реквизита при кастомном синониме ИЛИ наличии подсказки.
- spec meta-dsl-spec.md v2.3: §4.2 (tooltip) + §4.4 ML-значения (строка/{ru,en}), консистентно с form-compile.
- тест-кейс catalog-attr-ml (мультиязычный синоним + подсказка, обе формы).
Валидация: PS==PY паритет; полный прогон −228748 строк (447285→218537, >половины); регресс 35/35 (ps+py);
1С-cert зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
351 lines
17 KiB
PowerShell
351 lines
17 KiB
PowerShell
# meta-decompile v0.4 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
#
|
||
# Пилот: только Catalog. Инверс 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')
|
||
|
||
$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 -ne 'Catalog') {
|
||
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (пилот — только Catalog)"); 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
|
||
if ($lang -eq 'ru') { 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'
|
||
if ($next -and $next.LocalName -eq 'StringQualifiers') { $l = $next.SelectSingleNode('v8:Length', $nsm); if ($l) { $len = $l.InnerText } }
|
||
$parts += "String($len)"; break
|
||
}
|
||
'(^|:)decimal$' {
|
||
$d = '10'; $f = '0'; $sign = ''
|
||
if ($next -and $next.LocalName -eq 'NumberQualifiers') {
|
||
$dn = $next.SelectSingleNode('v8:Digits', $nsm); if ($dn) { $d = $dn.InnerText }
|
||
$fn = $next.SelectSingleNode('v8:FractionDigits', $nsm); if ($fn) { $f = $fn.InnerText }
|
||
$sn = $next.SelectSingleNode('v8:AllowedSign', $nsm); if ($sn -and $sn.InnerText -eq 'Nonnegative') { $sign = ',nonneg' }
|
||
}
|
||
$parts += "Number($d,$f$sign)"; break
|
||
}
|
||
'(^|:)dateTime$' {
|
||
$fr = 'DateTime'
|
||
if ($next -and $next.LocalName -eq 'DateQualifiers') { $dn = $next.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 ' + ')
|
||
}
|
||
|
||
# --- Реквизит → 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}). Кастомный синоним ИЛИ наличие подсказки → object-форма.
|
||
$synVal = Get-MLValue ($ap.SelectSingleNode('md:Synonym', $nsm))
|
||
$synCustom = $false
|
||
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))
|
||
|
||
if ($synCustom -or ($null -ne $ttVal)) {
|
||
$o = [ordered]@{ name = $nm }
|
||
if ($ts) { $o['type'] = $ts }
|
||
if ($synCustom) { $o['synonym'] = $synVal }
|
||
if ($null -ne $ttVal) { $o['tooltip'] = $ttVal }
|
||
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 = 'Catalog'; name = $objName }
|
||
|
||
$syn = Get-MLru ($props.SelectSingleNode('md:Synonym', $nsm))
|
||
if ($syn -and $syn -ne (Split-CamelWords $objName)) { $dsl['synonym'] = $syn }
|
||
$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'
|
||
Add-IntProp 'codeLength' 'CodeLength' 9
|
||
Add-IntProp 'descriptionLength' 'DescriptionLength' 25
|
||
Add-EnumProp 'codeType' 'CodeType' 'String'
|
||
Add-EnumProp 'codeAllowedLength' 'CodeAllowedLength' 'Variable'
|
||
Add-BoolProp 'autonumbering' 'Autonumbering' $true
|
||
Add-BoolProp 'checkUnique' 'CheckUnique' $false
|
||
Add-EnumProp 'codeSeries' 'CodeSeries' 'WholeCatalog'
|
||
Add-EnumProp 'defaultPresentation' 'DefaultPresentation' 'AsDescription'
|
||
Add-BoolProp 'quickChoice' 'QuickChoice' $false
|
||
Add-EnumProp 'choiceMode' 'ChoiceMode' 'BothWays'
|
||
Add-EnumProp 'dataLockControlMode' 'DataLockControlMode' 'Automatic'
|
||
Add-EnumProp 'fullTextSearch' 'FullTextSearch' 'Use'
|
||
|
||
# --- StandardAttributes: блок есть ⟺ кастомизация ≥1 стандартного реквизита.
|
||
# Захватываем ОТКЛОНЕНИЯ от профиля материализованного блока (профиль компилятор восстановит сам).
|
||
# Профиль Catalog: Owner{FC=ShowError,FFV=true}, Parent{FFV=true}, Description{FC=ShowError}. ---
|
||
$catStdProfile = @{
|
||
'Owner' = @{ fillChecking = 'ShowError'; fillFromFillingValue = $true }
|
||
'Parent' = @{ fillFromFillingValue = $true }
|
||
'Description' = @{ fillChecking = 'ShowError' }
|
||
}
|
||
$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 (профиль пуст)
|
||
$syn = Get-MLru ($sa.SelectSingleNode('xr:Synonym', $nsm))
|
||
if ($syn) { $ov['synonym'] = $syn }
|
||
# 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 }
|
||
if ($ov.Count -gt 0) { $saMap[$an] = $ov }
|
||
}
|
||
$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
|
||
}
|
||
$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)) } }
|
||
$tsMap[$tsName] = $cols
|
||
}
|
||
$dsl['tabularSections'] = $tsMap
|
||
}
|
||
}
|
||
|
||
# --- Предопределённые (соседний Ext/Predefined.xml). meta-compile пока их НЕ потребляет (пробел DSL).
|
||
# ⚠️ ЧЕРНОВАЯ/ВРЕМЕННАЯ форма `predefined` — предложение по факту корпуса, НЕ утверждённый DSL.
|
||
# Правило: непокрытый блок проектируем в DSL СНАЧАЛА (совместно), потом переписываем этот захват под него. ---
|
||
$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 Parse-PredefItem {
|
||
param($itemEl)
|
||
$o = [ordered]@{}
|
||
$o['name'] = ($itemEl.SelectSingleNode("*[local-name()='Name']")).InnerText
|
||
$codeEl = $itemEl.SelectSingleNode("*[local-name()='Code']")
|
||
if ($codeEl -and $codeEl.InnerText) { $o['code'] = $codeEl.InnerText }
|
||
$descEl = $itemEl.SelectSingleNode("*[local-name()='Description']")
|
||
if ($descEl -and $descEl.InnerText) { $o['description'] = $descEl.InnerText }
|
||
$folderEl = $itemEl.SelectSingleNode("*[local-name()='IsFolder']")
|
||
if ($folderEl -and $folderEl.InnerText -eq 'true') { $o['isFolder'] = $true }
|
||
$childContainer = $itemEl.SelectSingleNode("*[local-name()='ChildItems']")
|
||
if ($childContainer) {
|
||
$kids = [System.Collections.ArrayList]@()
|
||
foreach ($k in @($childContainer.SelectNodes("*[local-name()='Item']"))) { [void]$kids.Add((Parse-PredefItem $k)) }
|
||
if ($kids.Count -gt 0) { $o['children'] = $kids }
|
||
}
|
||
return $o
|
||
}
|
||
$rootItems = [System.Collections.ArrayList]@()
|
||
foreach ($it in @($pdoc.DocumentElement.SelectNodes("*[local-name()='Item']"))) { [void]$rootItems.Add((Parse-PredefItem $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)
|
||
}
|