Files
cc-1c-skills/.claude/skills/skd-decompile/scripts/skd-decompile.ps1
T
Nick Shirokov d8d80af88e feat(skd-decompile): категория A — SelectionItem implicit, StructureItemTable, StructureItemChart
- Build-Selection: implicit SelectedItemField при пустом xsi:type (платформа эмитит в conditionalAppearance)
- Build-Structure: ветки для StructureItemTable (columns/rows) и StructureItemChart (points/series/selection/outputParameters)
- Try-StructureShorthand: отказ от свёртки при type≠group
- Вспомогательные Get-GroupFields, Build-TableAxisBlock

На сэмпле 30 ERP-отчётов sentinel'ов категории A: 0 (было 640).
2026-05-21 17:03:31 +03:00

1582 lines
57 KiB
PowerShell

# skd-decompile v0.8 — Decompile 1C DCS Template.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- 0. Resolve and validate input ---
if (-not (Test-Path $TemplatePath)) {
Write-Error "Template not found: $TemplatePath"
exit 1
}
$TemplatePath = (Resolve-Path $TemplatePath).Path
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load($TemplatePath)
$root = $xmlDoc.DocumentElement
# Ring 3: not a DataCompositionSchema → fail-fast
if ($root.LocalName -ne 'DataCompositionSchema') {
[Console]::Error.WriteLine("skd-decompile: корневой элемент <$($root.LocalName)> не <DataCompositionSchema> — это не схема СКД (возможно, табличный документ — используй /mxl-decompile).")
exit 2
}
# --- 1. Namespace manager ---
$NS_SCHEMA = "http://v8.1c.ru/8.1/data-composition-system/schema"
$NS_COM = "http://v8.1c.ru/8.1/data-composition-system/common"
$NS_COR = "http://v8.1c.ru/8.1/data-composition-system/core"
$NS_SET = "http://v8.1c.ru/8.1/data-composition-system/settings"
$NS_AT = "http://v8.1c.ru/8.1/data-composition-system/area-template"
$NS_V8 = "http://v8.1c.ru/8.1/data/core"
$NS_V8UI = "http://v8.1c.ru/8.1/data/ui"
$NS_XS = "http://www.w3.org/2001/XMLSchema"
$NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
$NS_CFG = "http://v8.1c.ru/8.1/data/enterprise/current-config"
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("r", $NS_SCHEMA)
$ns.AddNamespace("dcscom", $NS_COM)
$ns.AddNamespace("dcscor", $NS_COR)
$ns.AddNamespace("dcsset", $NS_SET)
$ns.AddNamespace("dcsat", $NS_AT)
$ns.AddNamespace("v8", $NS_V8)
$ns.AddNamespace("v8ui", $NS_V8UI)
$ns.AddNamespace("xs", $NS_XS)
$ns.AddNamespace("xsi", $NS_XSI)
# --- 1b. Ring 3 scan: bail out on unsupported constructs ---
function Fail-Ring3 {
param([string]$kind, [string]$loc)
[Console]::Error.WriteLine("skd-decompile: декомпиляция не поддерживает $kind (path: $loc)")
[Console]::Error.WriteLine("Для точечной работы с этим отчётом используй /skd-edit.")
exit 3
}
# Picture cells in templates
foreach ($el in $xmlDoc.SelectNodes("//*[local-name()='item']")) {
$xsi = $el.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($xsi -match 'Picture$' -and $el.NamespaceURI -eq "http://v8.1c.ru/8.1/data-composition-system/area-template") {
Fail-Ring3 -kind "Picture cell в шаблоне" -loc "template/.../item[@xsi:type=Picture]"
}
}
# ValueStorage parameter type
foreach ($vt in $xmlDoc.SelectNodes("//*[local-name()='Type']")) {
$inner = $vt.InnerText
if ($inner -match '^v8:ValueStorage$|:ValueStorage$') {
Fail-Ring3 -kind "параметр типа ХранилищеЗначения" -loc "valueType[v8:Type=ValueStorage]"
}
}
# templateCondition (variant templates) — top-level <template> with <templateCondition>
foreach ($t in $xmlDoc.SelectNodes("//*[local-name()='templateCondition']")) {
Fail-Ring3 -kind "templateCondition (вариативные шаблоны)" -loc "template/templateCondition"
}
# --- 2. Warnings accumulator ---
$script:warnings = @()
$script:warningCounter = 0
function Add-Warning {
param([string]$kind, [string]$loc, [string]$detail)
$script:warningCounter++
$id = "W{0:D3}" -f $script:warningCounter
$script:warnings += [ordered]@{ id = $id; kind = $kind; loc = $loc; detail = $detail }
return $id
}
function New-Sentinel {
param([string]$kind, [string]$loc, [string]$detail)
$id = Add-Warning -kind $kind -loc $loc -detail $detail
return [ordered]@{ '__unsupported__' = [ordered]@{ id = $id; kind = $kind; loc = $loc } }
}
# --- 3. Helpers ---
function Get-Text {
param($node, [string]$xpath)
if (-not $node) { return $null }
$n = $node.SelectSingleNode($xpath, $ns)
if ($n) { return $n.InnerText } else { return $null }
}
# Extract LocalStringType (multilingual title) → string (if only ru) or hashtable
function Get-MLText {
param($node)
if (-not $node) { return $null }
$items = $node.SelectNodes("v8:item", $ns)
if ($items.Count -eq 0) { return $null }
$dict = [ordered]@{}
foreach ($it in $items) {
$lang = Get-Text $it "v8:lang"
$content = Get-Text $it "v8:content"
if ($lang) { $dict[$lang] = if ($content) { $content } else { "" } }
}
if ($dict.Count -eq 1 -and $dict.Contains('ru')) { return $dict['ru'] }
return $dict
}
# Strip namespace prefix from xsi:type value (e.g. "dcsset:Foo" → "Foo")
function Get-LocalXsiType {
param($node)
if (-not $node) { return $null }
$t = $node.GetAttribute("type", $NS_XSI)
if ($t -match ':(.+)$') { return $matches[1] }
return $t
}
# Convert one <v8:Type> element + sibling qualifiers → shorthand type string
function Get-OneTypeShorthand {
param($typeNode, $qualNumber, $qualString, $qualDate)
$raw = $typeNode.InnerText.Trim()
# Strip namespace prefix; check if it's d5p1: (config refs)
$local = $raw
if ($raw -match '^([^:]+):(.+)$') {
$prefix = $matches[1]
$local = $matches[2]
# Resolve prefix → namespace URI
$uri = $typeNode.GetNamespaceOfPrefix($prefix)
if ($uri -eq $NS_CFG) {
return $local # CatalogRef.X, DocumentRef.X, etc.
}
if ($uri -eq $NS_XS) {
switch ($local) {
'string' {
if ($qualString) {
$len = [int](Get-Text $qualString "v8:Length")
$allowed = Get-Text $qualString "v8:AllowedLength"
if ($len -eq 0) { return 'string' }
if ($allowed -eq 'Fixed') { return "string($len,fix)" }
return "string($len)"
}
return 'string'
}
'boolean' { return 'boolean' }
'decimal' {
if ($qualNumber) {
$d = [int](Get-Text $qualNumber "v8:Digits")
$f = [int](Get-Text $qualNumber "v8:FractionDigits")
$sign = Get-Text $qualNumber "v8:AllowedSign"
$signSuf = ''
if ($sign -eq 'Nonnegative') { $signSuf = ',nonneg' }
# Always explicit (D,F) — JSON readable, no surprise from default folding
if ($f -eq 0) { return "decimal($d$signSuf)" }
if ($signSuf) { return "decimal($d,$f$signSuf)" }
return "decimal($d,$f)"
}
return 'decimal'
}
'dateTime' {
$frac = if ($qualDate) { Get-Text $qualDate "v8:DateFractions" } else { 'DateTime' }
switch ($frac) {
'Date' { return 'date' }
'Time' { return 'time' }
default { return 'dateTime' }
}
}
default { return $local }
}
}
if ($uri -eq $NS_V8) {
# v8:StandardPeriod, etc.
return $local
}
}
return $local
}
# valueType → string shorthand OR array of shorthands (composite)
function Get-ValueTypeShorthand {
param($valueTypeNode)
if (-not $valueTypeNode) { return $null }
$types = $valueTypeNode.SelectNodes("v8:Type", $ns)
if ($types.Count -eq 0) { return $null }
$qualN = $valueTypeNode.SelectSingleNode("v8:NumberQualifiers", $ns)
$qualS = $valueTypeNode.SelectSingleNode("v8:StringQualifiers", $ns)
$qualD = $valueTypeNode.SelectSingleNode("v8:DateQualifiers", $ns)
$shorts = @()
foreach ($t in $types) { $shorts += (Get-OneTypeShorthand -typeNode $t -qualNumber $qualN -qualString $qualS -qualDate $qualD) }
if ($shorts.Count -eq 1) { return $shorts[0] }
return ,$shorts
}
# <role> → array of @tokens; if non-simple — null + sentinel via $script:roleSentinel
function Get-RoleTokens {
param($roleNode, [string]$loc)
if (-not $roleNode) { return $null }
$tokens = @()
$hasComplex = $false
foreach ($child in $roleNode.ChildNodes) {
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if ($child.NamespaceURI -ne $NS_COM) { $hasComplex = $true; continue }
switch ($child.LocalName) {
'dimension' { if ($child.InnerText -eq 'true') { $tokens += '@dimension' } }
'account' { if ($child.InnerText -eq 'true') { $tokens += '@account' } }
'balance' { if ($child.InnerText -eq 'true') { $tokens += '@balance' } }
'periodNumber' {
# Expect periodNumber=1 + periodType=Main → @period
$pType = Get-Text $roleNode "dcscom:periodType"
if ($child.InnerText -eq '1' -and $pType -eq 'Main') { $tokens += '@period' } else { $hasComplex = $true }
}
'periodType' { } # handled with periodNumber above
default {
$hasComplex = $true
}
}
}
if ($hasComplex) {
# emit sentinel separately so caller can attach it to field obj
$null = New-Sentinel -kind 'ComplexRole' -loc $loc -detail 'Роль с дополнительными атрибутами не сворачивается в @-флаг'
}
return $tokens
}
# <useRestriction> → array of #tokens
function Get-RestrictionTokens {
param($urNode)
if (-not $urNode) { return @() }
$tokens = @()
$map = @{ 'field' = '#noField'; 'condition' = '#noFilter'; 'group' = '#noGroup'; 'order' = '#noOrder' }
foreach ($key in 'field','condition','group','order') {
$v = Get-Text $urNode "r:$key"
if ($v -eq 'true') { $tokens += $map[$key] }
}
return $tokens
}
# <appearance> → hashtable {param: value}
function Get-AppearanceDict {
param($appNode)
if (-not $appNode) { return $null }
$dict = [ordered]@{}
$items = $appNode.SelectNodes("dcscor:item", $ns)
foreach ($it in $items) {
$p = Get-Text $it "dcscor:parameter"
$valNode = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $p -or -not $valNode) { continue }
# Value can be xs:string, v8ui:HorizontalAlign, v8:LocalStringType, etc.
$valType = Get-LocalXsiType $valNode
if ($valType -eq 'LocalStringType') {
$dict[$p] = Get-MLText $valNode
} else {
$dict[$p] = $valNode.InnerText
}
}
return $dict
}
# Build a field JSON entry (shorthand if possible, object form otherwise)
function Build-Field {
param($fieldNode, [string]$loc)
$dataPath = Get-Text $fieldNode "r:dataPath"
$fieldName = Get-Text $fieldNode "r:field"
$titleNode = $fieldNode.SelectSingleNode("r:title", $ns)
$title = Get-MLText $titleNode
$valueTypeNode = $fieldNode.SelectSingleNode("r:valueType", $ns)
$typeShort = Get-ValueTypeShorthand $valueTypeNode
$roleTokens = Get-RoleTokens $fieldNode.SelectSingleNode("r:role", $ns) "$loc/role"
$restrictTokens = Get-RestrictionTokens $fieldNode.SelectSingleNode("r:useRestriction", $ns)
$appNode = $fieldNode.SelectSingleNode("r:appearance", $ns)
$appearance = Get-AppearanceDict $appNode
$presExpr = Get-Text $fieldNode "r:presentationExpression"
$needsObject = $title -or $appearance -or $presExpr -or ($typeShort -is [array])
if (-not $needsObject) {
# shorthand: "Name: type @role #restrict"
$parts = @($fieldName)
if ($typeShort) { $parts[0] = "$fieldName`: $typeShort" }
if ($roleTokens) { $parts[0] += ' ' + ($roleTokens -join ' ') }
if ($restrictTokens) { $parts[0] += ' ' + ($restrictTokens -join ' ') }
# dataPath ≠ field — fall back to object form
if ($dataPath -and $dataPath -ne $fieldName) {
# unusual case; use object form
} else {
return $parts[0]
}
}
$obj = [ordered]@{ field = $fieldName }
if ($dataPath -and $dataPath -ne $fieldName) { $obj['dataPath'] = $dataPath }
if ($title) { $obj['title'] = $title }
if ($typeShort) { $obj['type'] = $typeShort }
if ($roleTokens) {
if ($roleTokens.Count -eq 1) { $obj['role'] = $roleTokens[0] -replace '^@','' }
else { $obj['role'] = ($roleTokens | ForEach-Object { $_ -replace '^@','' }) }
}
if ($restrictTokens) { $obj['restrict'] = ($restrictTokens | ForEach-Object { $_ -replace '^#','' }) }
if ($presExpr) { $obj['presentationExpression'] = $presExpr }
if ($appearance) { $obj['appearance'] = $appearance }
return $obj
}
# Build calculatedField → shorthand string or object form
function Build-CalcField {
param($cfNode, [string]$loc)
$dataPath = Get-Text $cfNode "r:dataPath"
$expression = Get-Text $cfNode "r:expression"
$titleNode = $cfNode.SelectSingleNode("r:title", $ns)
$title = Get-MLText $titleNode
$valueTypeNode = $cfNode.SelectSingleNode("r:valueType", $ns)
$typeShort = Get-ValueTypeShorthand $valueTypeNode
$restrictTokens = Get-RestrictionTokens $cfNode.SelectSingleNode("r:useRestriction", $ns)
$appNode = $cfNode.SelectSingleNode("r:appearance", $ns)
$appearance = Get-AppearanceDict $appNode
# multilingual title (non-ru) → object form
$titleNeedsObject = ($title -is [System.Collections.IDictionary]) -or ($typeShort -is [array])
$needsObject = $appearance -or $titleNeedsObject
if (-not $needsObject) {
# shorthand: "Name [Title]: type = expression #restrict"
$s = $dataPath
if ($title) { $s += " [$title]" }
if ($typeShort) { $s += ": $typeShort" }
if ($expression) { $s += " = $expression" }
if ($restrictTokens) { $s += ' ' + ($restrictTokens -join ' ') }
return $s
}
$obj = [ordered]@{ name = $dataPath }
if ($title) { $obj['title'] = $title }
if ($typeShort) { $obj['type'] = $typeShort }
if ($expression) { $obj['expression'] = $expression }
if ($restrictTokens) { $obj['restrict'] = ($restrictTokens | ForEach-Object { $_ -replace '^#','' }) }
if ($appearance) { $obj['appearance'] = $appearance }
return $obj
}
# Build totalField → shorthand or object form
function Build-TotalField {
param($tfNode)
$dataPath = Get-Text $tfNode "r:dataPath"
$expression = Get-Text $tfNode "r:expression"
# Detect Func(<dataPath>) → shorthand "name: Func"
if ($expression -match '^(\w+)\(([^)]*)\)$') {
$func = $matches[1]
$inner = $matches[2].Trim()
if ($inner -eq $dataPath) {
return "$dataPath`: $func"
}
# "name: Func(expr)" form — also a valid shorthand
return "$dataPath`: $func($inner)"
}
# group attachment via groupItem — Ring 2 / object form
$groupNodes = $tfNode.SelectNodes("r:group", $ns)
$obj = [ordered]@{ dataPath = $dataPath; expression = $expression }
if ($groupNodes -and $groupNodes.Count -gt 0) {
$groups = @()
foreach ($g in $groupNodes) { $groups += $g.InnerText }
$obj['group'] = $groups
}
return $obj
}
# Detect StandardPeriod variant from <value> node
function Get-StandardPeriodVariant {
param($valueNode)
if (-not $valueNode) { return $null }
$variant = Get-Text $valueNode "v8:variant"
if ($variant) { return $variant }
return $null
}
# Build parameter → shorthand or object form
function Build-Parameter {
param($pNode, [string]$loc)
$name = Get-Text $pNode "r:name"
$titleNode = $pNode.SelectSingleNode("r:title", $ns)
$title = Get-MLText $titleNode
$valueTypeNode = $pNode.SelectSingleNode("r:valueType", $ns)
$typeShort = Get-ValueTypeShorthand $valueTypeNode
# value
$valueNode = $pNode.SelectSingleNode("r:value", $ns)
$valueDisplay = $null
$valueIsNil = $false
if ($valueNode) {
$nil = $valueNode.GetAttribute("nil", $NS_XSI)
if ($nil -eq 'true') { $valueIsNil = $true }
else {
$vType = Get-LocalXsiType $valueNode
if ($vType -eq 'StandardPeriod') {
$variant = Get-Text $valueNode "v8:variant"
if ($variant -and $variant -ne 'Custom') { $valueDisplay = $variant }
# Custom with explicit dates → object form (handled below via needsObject)
} elseif ($vType -eq 'DesignTimeValue') {
$valueDisplay = $valueNode.InnerText
} elseif ($vType -eq 'LocalStringType') {
$valueDisplay = Get-MLText $valueNode
} else {
$txt = $valueNode.InnerText
if ($txt) { $valueDisplay = $txt }
}
}
}
$valueListAllowed = (Get-Text $pNode "r:valueListAllowed") -eq 'true'
$availableAsField = Get-Text $pNode "r:availableAsField"
$hidden = $availableAsField -eq 'false'
$denyIncomplete = (Get-Text $pNode "r:denyIncompleteValues") -eq 'true'
$useAttr = Get-Text $pNode "r:use"
$useRestriction = (Get-Text $pNode "r:useRestriction") -eq 'true'
$expression = Get-Text $pNode "r:expression"
# availableValues
$avNodes = $pNode.SelectNodes("r:availableValue", $ns)
$availableValues = @()
foreach ($av in $avNodes) {
$avValNode = $av.SelectSingleNode("r:value", $ns)
$avPresNode = $av.SelectSingleNode("r:presentation", $ns)
$avEntry = [ordered]@{}
if ($avValNode) { $avEntry['value'] = $avValNode.InnerText }
if ($avPresNode) { $avEntry['presentation'] = Get-MLText $avPresNode }
$availableValues += $avEntry
}
$flags = @()
$result = [ordered]@{
name = $name
title = $title
typeShort = $typeShort
valueDisplay = $valueDisplay
valueIsNil = $valueIsNil
valueListAllowed = $valueListAllowed
hidden = $hidden
denyIncomplete = $denyIncomplete
useAttr = $useAttr
useRestriction = $useRestriction
expression = $expression
availableValues = $availableValues
}
return $result
}
# Render parameter (after autoDates folding) → shorthand or object form
function Render-Parameter {
param($p)
$name = $p.name
$title = $p.title
$typeShort = $p.typeShort
$valueDisplay = $p.valueDisplay
$valueIsNil = $p.valueIsNil
$flags = @()
if ($p.autoDates) { $flags += '@autoDates' }
if ($p.valueListAllowed) { $flags += '@valueList' }
if ($p.hidden) { $flags += '@hidden' }
$titleNeedsObject = ($title -is [System.Collections.IDictionary])
$typeIsArray = ($typeShort -is [array])
$valueIsDict = ($valueDisplay -is [System.Collections.IDictionary])
# Object form needed if: availableValues, multilingual title, composite type,
# explicit denyIncomplete/use without @autoDates, useRestriction without autoDates, expression set
$needsObject = $false
if ($p.availableValues -and $p.availableValues.Count -gt 0) { $needsObject = $true }
if ($titleNeedsObject) { $needsObject = $true }
if ($typeIsArray) { $needsObject = $true }
if ($valueIsDict) { $needsObject = $true }
if (-not $p.autoDates) {
# @autoDates implies use=Always + denyIncomplete=true defaults — only object form if NOT autoDates
if ($p.denyIncomplete) { $needsObject = $true }
if ($p.useAttr) { $needsObject = $true }
}
# useRestriction is auto-generated by compile for @hidden params; ignore as object trigger
if ($p.expression) { $needsObject = $true }
if (-not $needsObject) {
$s = $name
if ($title) { $s += " [$title]" }
if ($typeShort) { $s += ": $typeShort" }
if (-not $valueIsNil -and $null -ne $valueDisplay -and $valueDisplay -ne '') { $s += " = $valueDisplay" }
if ($flags) { $s += ' ' + ($flags -join ' ') }
return $s
}
$obj = [ordered]@{ name = $name }
if ($title) { $obj['title'] = $title }
if ($typeShort) { $obj['type'] = $typeShort }
if (-not $valueIsNil -and $null -ne $valueDisplay -and $valueDisplay -ne '') { $obj['value'] = $valueDisplay }
if ($p.useAttr -and -not $p.autoDates) { $obj['use'] = $p.useAttr }
if ($p.denyIncomplete -and -not $p.autoDates) { $obj['denyIncompleteValues'] = $true }
if ($p.hidden) { $obj['hidden'] = $true }
if ($p.valueListAllowed) { $obj['valueListAllowed'] = $true }
if ($p.autoDates) { $obj['autoDates'] = $true }
if ($p.expression) { $obj['expression'] = $p.expression }
if ($p.availableValues -and $p.availableValues.Count -gt 0) { $obj['availableValues'] = $p.availableValues }
return $obj
}
# --- 3b. Built-in style fingerprints ---
# Fingerprints for built-in styles (header/data/subheader/total).
# Each is the set of "shape-defining" appearance items the style emits.
# Width/Height/merge flags are excluded — they're per-cell.
$script:builtinStyleFingerprints = @{
'header' = @{
ЦветФона = 'd8p1:ReportHeaderBackColor'
ЦветГраницы = 'd8p1:ReportLineColor'
СтильГраницы = 'None|0'
'СтильГраницы.Слева' = 'Solid|1'
'СтильГраницы.Сверху' = 'Solid|1'
'СтильГраницы.Справа' = 'Solid|1'
'СтильГраницы.Снизу' = 'Solid|1'
Шрифт = 'Arial|10|false|false|false|false'
ГоризонтальноеПоложение = 'Center'
Размещение = 'Wrap'
}
'data' = @{
ЦветФона = 'd8p1:ReportGroup1BackColor'
ЦветГраницы = 'd8p1:ReportLineColor'
СтильГраницы = 'None|0'
'СтильГраницы.Слева' = 'Solid|1'
'СтильГраницы.Сверху' = 'Solid|1'
'СтильГраницы.Справа' = 'Solid|1'
'СтильГраницы.Снизу' = 'Solid|1'
Шрифт = 'Arial|10|false|false|false|false'
}
'subheader' = @{
ЦветГраницы = 'd8p1:ReportLineColor'
СтильГраницы = 'None|0'
'СтильГраницы.Слева' = 'Solid|1'
'СтильГраницы.Сверху' = 'Solid|1'
'СтильГраницы.Справа' = 'Solid|1'
'СтильГраницы.Снизу' = 'Solid|1'
Шрифт = 'Arial|10|false|false|false|false'
ГоризонтальноеПоложение = 'Center'
}
'total' = @{
ЦветГраницы = 'd8p1:ReportLineColor'
СтильГраницы = 'None|0'
'СтильГраницы.Слева' = 'Solid|1'
'СтильГраницы.Сверху' = 'Solid|1'
'СтильГраницы.Справа' = 'Solid|1'
'СтильГраницы.Снизу' = 'Solid|1'
Шрифт = 'Arial|10|false|false|false|false'
}
}
# Normalize an <dcsat:appearance> node to a hashtable of "shape" keys (excluding
# per-cell items: widths, heights, merge flags, drilldown). Used for style matching.
function Get-AppearanceFingerprint {
param($appNode)
$fp = @{}
if (-not $appNode) { return $fp }
# Top-level dcscor:item children
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
$pName = Get-Text $it "dcscor:parameter"
$val = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $pName -or -not $val) { continue }
# Skip per-cell keys
if ($pName -in @('МинимальнаяШирина','МаксимальнаяШирина','МинимальнаяВысота','ОбъединятьПоВертикали','ОбъединятьПоГоризонтали','Расшифровка')) { continue }
$valType = Get-LocalXsiType $val
switch ($valType) {
'Color' { $fp[$pName] = $val.InnerText }
'Line' {
$w = $val.GetAttribute("width")
$styleNode = $val.SelectSingleNode("v8ui:style", $ns)
$lineStyle = if ($styleNode) { $styleNode.InnerText } else { '' }
$fp[$pName] = "$lineStyle|$w"
}
'Font' {
$face = $val.GetAttribute("faceName")
$h = $val.GetAttribute("height")
$b = $val.GetAttribute("bold")
$i = $val.GetAttribute("italic")
$u = $val.GetAttribute("underline")
$s = $val.GetAttribute("strikeout")
$fp[$pName] = "$face|$h|$b|$i|$u|$s"
}
default { $fp[$pName] = $val.InnerText }
}
# Nested sub-items under СтильГраницы (left/top/right/bottom)
foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
$subName = Get-Text $sub "dcscor:parameter"
$subVal = $sub.SelectSingleNode("dcscor:value", $ns)
if (-not $subName -or -not $subVal) { continue }
$subType = Get-LocalXsiType $subVal
if ($subType -eq 'Line') {
$w = $subVal.GetAttribute("width")
$styleNode = $subVal.SelectSingleNode("v8ui:style", $ns)
$lineStyle = if ($styleNode) { $styleNode.InnerText } else { '' }
$fp[$subName] = "$lineStyle|$w"
}
}
}
# Translate d8p1: colors to canonical "d8p1:Name" form (InnerText already contains prefix)
return $fp
}
# Check if appearance fingerprint matches a built-in style.
# Returns style name or $null.
function Match-BuiltinStyle {
param($fp)
foreach ($styleName in $script:builtinStyleFingerprints.Keys) {
$expected = $script:builtinStyleFingerprints[$styleName]
$match = $true
# All expected keys must be present with matching value
foreach ($k in $expected.Keys) {
if (-not $fp.ContainsKey($k) -or $fp[$k] -ne $expected[$k]) { $match = $false; break }
}
if (-not $match) { continue }
# Must not have extra keys that aren't in the expected fingerprint
$extras = @($fp.Keys | Where-Object { -not $expected.ContainsKey($_) })
if ($extras.Count -ne 0) { continue }
return $styleName
}
return $null
}
# Extract per-cell width/minHeight/merge from appearance.
function Get-CellPerCellAttrs {
param($appNode)
$attrs = @{ width = $null; height = $null; mergeV = $false; mergeH = $false; drilldown = $null }
if (-not $appNode) { return $attrs }
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
$pName = Get-Text $it "dcscor:parameter"
$val = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $pName) { continue }
switch ($pName) {
'МинимальнаяШирина' { if ($val) { $attrs.width = $val.InnerText } }
'МинимальнаяВысота' { if ($val) { $attrs.height = $val.InnerText } }
'ОбъединятьПоВертикали' { if ($val -and $val.InnerText -eq 'true') { $attrs.mergeV = $true } }
'ОбъединятьПоГоризонтали' { if ($val -and $val.InnerText -eq 'true') { $attrs.mergeH = $true } }
'Расшифровка' {
# value xsi:type=dcscor:Parameter pointing to Расшифровка_X
if ($val) {
$paramRef = $val.InnerText
if ($paramRef -match '^Расшифровка_(.+)$') { $attrs.drilldown = $matches[1] }
}
}
}
}
return $attrs
}
# Extract cell content: string text, "{ParamName}", "|", ">", or $null
function Get-CellContent {
param($cellNode, $perCellAttrs)
# Check merge flags first — empty cells with these flags are "|" or ">"
if ($perCellAttrs.mergeV) { return '|' }
if ($perCellAttrs.mergeH) { return '>' }
$item = $cellNode.SelectSingleNode("dcsat:item", $ns)
if (-not $item) { return $null }
$itemType = Get-LocalXsiType $item
$valNode = $item.SelectSingleNode("dcsat:value", $ns)
if (-not $valNode) { return $null }
$valType = Get-LocalXsiType $valNode
if ($itemType -eq 'Field' -and $valType -eq 'Parameter') {
return '{' + $valNode.InnerText + '}'
}
if ($valType -eq 'LocalStringType') {
$text = Get-MLText $valNode
if ($text -is [System.Collections.IDictionary]) {
# multilang in template cell — keep as-is; emit via object form (Ring 2 candidate)
return $text
}
return $text
}
# Fallback: take inner text
return $valNode.InnerText
}
# Build template parameter entry. Returns hashtable with `name` + `expression` (+ optional `drilldown`)
function Build-TemplateParameter {
param($pNode)
$pType = Get-LocalXsiType $pNode
$obj = [ordered]@{}
$obj['name'] = Get-Text $pNode "dcsat:name"
if ($pType -eq 'ExpressionAreaTemplateParameter') {
$obj['expression'] = Get-Text $pNode "dcsat:expression"
} elseif ($pType -eq 'DetailsAreaTemplateParameter') {
# Marker — handled by drilldown folding logic in Build-Template
$obj['__details__'] = $true
$obj['expression'] = Get-Text $pNode "dcsat:expression"
}
return $obj
}
# Build template entry from <template> node
function Build-Template {
param($templateNode, [string]$loc)
$tmplObj = [ordered]@{ name = Get-Text $templateNode "r:name" }
$inner = $templateNode.SelectSingleNode("r:template", $ns)
if (-not $inner) { return $tmplObj }
# Walk rows
$rowNodes = $inner.SelectNodes("dcsat:item[@xsi:type='dcsat:TableRow']", $ns)
# fallback: any dcsat:item (in case xsi prefix differs)
if ($rowNodes.Count -eq 0) {
$allItems = $inner.SelectNodes("dcsat:item", $ns)
$rowNodes = @()
foreach ($n in $allItems) { if ((Get-LocalXsiType $n) -eq 'TableRow') { $rowNodes += $n } }
}
$rows = @()
$widths = $null
$minHeight = $null
$detectedStyle = $null
$styleMismatch = $false
$drilldownByParam = @{} # param name → field name (X from Расшифровка_X)
$rowIdx = 0
foreach ($rowNode in $rowNodes) {
$cells = @()
$cellNodes = $rowNode.SelectNodes("dcsat:tableCell", $ns)
$colIdx = 0
# First-row collects widths
$rowWidths = @()
foreach ($cellNode in $cellNodes) {
$appNode = $cellNode.SelectSingleNode("dcsat:appearance", $ns)
$perCell = Get-CellPerCellAttrs $appNode
$content = Get-CellContent $cellNode $perCell
# Style detection (skip empty cells with no appearance, and merge cells)
if ($appNode -and -not $perCell.mergeV -and -not $perCell.mergeH) {
$fp = Get-AppearanceFingerprint $appNode
$matched = Match-BuiltinStyle $fp
if ($null -eq $detectedStyle) {
$detectedStyle = $matched
} elseif ($matched -ne $detectedStyle) {
$styleMismatch = $true
}
}
# Drilldown attachment
if ($content -match '^\{(.+)\}$' -and $perCell.drilldown) {
$drilldownByParam[$matches[1]] = $perCell.drilldown
}
# First row collects widths from any non-merge cell
if ($rowIdx -eq 0 -and $perCell.width) { $rowWidths += $perCell.width }
# First row collects minHeight from the first non-empty cell
if ($rowIdx -eq 0 -and $colIdx -eq 0 -and $perCell.height) { $minHeight = $perCell.height }
$cells += $content
$colIdx++
}
if ($rowIdx -eq 0 -and $rowWidths.Count -gt 0) { $widths = $rowWidths }
$rows += ,$cells
$rowIdx++
}
# Template parameters (and drilldown folding)
$paramNodes = $templateNode.SelectNodes("r:parameter", $ns)
$exprParams = [ordered]@{}
$detailParams = @{}
foreach ($pn in $paramNodes) {
$pType = Get-LocalXsiType $pn
$pName = Get-Text $pn "dcsat:name"
if ($pType -eq 'ExpressionAreaTemplateParameter') {
$exprParams[$pName] = Get-Text $pn "dcsat:expression"
} elseif ($pType -eq 'DetailsAreaTemplateParameter') {
# Name format: Расшифровка_<X>
if ($pName -match '^Расшифровка_(.+)$') {
$detailParams[$matches[1]] = $true
}
}
}
$templateParams = @()
foreach ($pname in $exprParams.Keys) {
$entry = [ordered]@{ name = $pname; expression = $exprParams[$pname] }
if ($drilldownByParam.ContainsKey($pname)) {
$entry['drilldown'] = $drilldownByParam[$pname]
}
$templateParams += $entry
}
# Decide output form
if ($detectedStyle -and -not $styleMismatch) {
$tmplObj['style'] = $detectedStyle
} elseif ($styleMismatch -or ($null -eq $detectedStyle -and $rows.Count -gt 0)) {
# Couldn't unify style — emit sentinel
$tmplObj['__unsupported__'] = (New-Sentinel -kind 'TemplateStyleMismatch' -loc $loc -detail 'Шаблон содержит ячейки с непокрытым/неоднородным оформлением (Кольцо 2)')['__unsupported__']
}
if ($widths) { $tmplObj['widths'] = $widths }
if ($minHeight) { $tmplObj['minHeight'] = $minHeight }
$tmplObj['rows'] = $rows
if ($templateParams.Count -gt 0) { $tmplObj['parameters'] = $templateParams }
return $tmplObj
}
# --- 3c. Filter / settings helpers ---
$script:filterOpMap = @{
'Equal'='='; 'NotEqual'='<>'; 'Greater'='>'; 'GreaterOrEqual'='>=';
'Less'='<'; 'LessOrEqual'='<='; 'InList'='in'; 'NotInList'='notIn';
'InHierarchy'='inHierarchy'; 'InListByHierarchy'='inListByHierarchy';
'Contains'='contains'; 'NotContains'='notContains';
'BeginsWith'='beginsWith'; 'NotBeginsWith'='notBeginsWith';
'Filled'='filled'; 'NotFilled'='notFilled'
}
# Render a filter value node to a shorthand-acceptable scalar string
function Get-FilterValue {
param($valNode)
if (-not $valNode) { return '_' }
$nil = $valNode.GetAttribute("nil", $NS_XSI)
if ($nil -eq 'true') { return '_' }
$vType = Get-LocalXsiType $valNode
if ($vType -eq 'DesignTimeValue') { return $valNode.InnerText }
if ($vType -eq 'LocalStringType') { return (Get-MLText $valNode) }
$txt = $valNode.InnerText
if (-not $txt) { return '_' }
return $txt
}
# Convert filter item node → shorthand string or object form
function Build-FilterItem {
param($itemNode, [string]$loc)
$xtype = Get-LocalXsiType $itemNode
if ($xtype -eq 'FilterItemGroup') {
$gt = Get-Text $itemNode "dcsset:groupType"
$groupName = switch ($gt) { 'OrGroup' { 'Or' } 'NotGroup' { 'Not' } default { 'And' } }
$items = @()
foreach ($c in $itemNode.SelectNodes("dcsset:item", $ns)) {
$items += (Build-FilterItem -itemNode $c -loc "$loc/item")
}
return [ordered]@{ group = $groupName; items = $items }
}
if ($xtype -ne 'FilterItemComparison') {
return (New-Sentinel -kind "FilterItemType:$xtype" -loc $loc -detail 'Неизвестный тип фильтра')
}
$leftNode = $itemNode.SelectSingleNode("dcsset:left", $ns)
$field = if ($leftNode) { $leftNode.InnerText } else { $null }
$ct = Get-Text $itemNode "dcsset:comparisonType"
$op = $script:filterOpMap[$ct]
if (-not $op) { $op = $ct }
$rightNode = $itemNode.SelectSingleNode("dcsset:right", $ns)
$value = Get-FilterValue $rightNode
$use = Get-Text $itemNode "dcsset:use"
$userId = Get-Text $itemNode "dcsset:userSettingID"
$viewMode = Get-Text $itemNode "dcsset:viewMode"
$userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
$flags = @()
if ($use -eq 'false') { $flags += '@off' }
if ($userId) { $flags += '@user' }
if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' }
elseif ($viewMode -eq 'Normal') { $flags += '@normal' }
elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' }
# nullity ops have no value
$noValueOps = @('filled','notFilled')
if ($userPresNode) {
# object form
$obj = [ordered]@{ field = $field; op = $op }
if ($op -notin $noValueOps -and $null -ne $value) { $obj['value'] = $value }
if ($use -eq 'false') { $obj['use'] = $false }
if ($userId) { $obj['userSettingID'] = 'auto' }
if ($viewMode) { $obj['viewMode'] = $viewMode }
$obj['userSettingPresentation'] = Get-MLText $userPresNode
return $obj
}
# shorthand
$s = $field
if ($op -in $noValueOps) {
$s += " $op"
} else {
$s += " $op $value"
}
if ($flags) { $s += ' ' + ($flags -join ' ') }
return $s
}
# Build selection items array
function Build-Selection {
param($selNode, [string]$loc)
if (-not $selNode) { return @() }
$out = @()
foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) {
$xt = Get-LocalXsiType $it
# Implicit SelectedItemField: <dcsset:item> without xsi:type but with <dcsset:field> child.
# Платформа эмитит это в conditionalAppearance/selection.
if (-not $xt) {
$fName = Get-Text $it "dcsset:field"
if ($fName) { $out += $fName; continue }
}
switch ($xt) {
'SelectedItemAuto' { $out += 'Auto' }
'SelectedItemField' { $out += (Get-Text $it "dcsset:field") }
'SelectedItemFolder' {
$titleNode = $it.SelectSingleNode("dcsset:lwsTitle", $ns)
$folderTitle = Get-MLText $titleNode
$inner = @()
foreach ($sub in $it.SelectNodes("dcsset:item", $ns)) {
$st = Get-LocalXsiType $sub
if (-not $st) {
$subF = Get-Text $sub "dcsset:field"
if ($subF) { $inner += $subF; continue }
}
if ($st -eq 'SelectedItemField') { $inner += (Get-Text $sub "dcsset:field") }
elseif ($st -eq 'SelectedItemAuto') { $inner += 'Auto' }
else { $inner += (New-Sentinel -kind "SelectionInFolder:$st" -loc "$loc/folder" -detail 'Неизвестный тип элемента папки выбора') }
}
$out += [ordered]@{ folder = $folderTitle; items = $inner }
}
default {
$out += (New-Sentinel -kind "SelectionItem:$xt" -loc $loc -detail 'Неизвестный тип элемента selection')
}
}
}
return ,$out
}
# Build order items array
function Build-Order {
param($ordNode, [string]$loc)
if (-not $ordNode) { return @() }
$out = @()
foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) {
$xt = Get-LocalXsiType $it
switch ($xt) {
'OrderItemAuto' { $out += 'Auto' }
'OrderItemField' {
$fn = Get-Text $it "dcsset:field"
$ot = Get-Text $it "dcsset:orderType"
if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
}
default { $out += (New-Sentinel -kind "OrderItem:$xt" -loc $loc -detail 'Неизвестный тип сортировки') }
}
}
return ,$out
}
# Build appearance dict from <dcsset:appearance> or <dcscor:item> list
function Get-SettingsAppearance {
param($appNode)
if (-not $appNode) { return $null }
$dict = [ordered]@{}
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
$pName = Get-Text $it "dcscor:parameter"
$val = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $pName -or -not $val) { continue }
$valType = Get-LocalXsiType $val
if ($valType -eq 'LocalStringType') {
$dict[$pName] = Get-MLText $val
} else {
$dict[$pName] = $val.InnerText
}
}
return $dict
}
# Build conditionalAppearance array
function Build-ConditionalAppearance {
param($caNode, [string]$loc)
if (-not $caNode) { return @() }
$out = @()
$i = 0
foreach ($it in $caNode.SelectNodes("dcsset:item", $ns)) {
$entry = [ordered]@{}
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
if ($selNode -and $selNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$entry['selection'] = Build-Selection -selNode $selNode -loc "$loc/$i/selection"
}
$filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$f = @()
foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) {
$f += (Build-FilterItem -itemNode $fc -loc "$loc/$i/filter")
}
$entry['filter'] = $f
}
$appNode = $it.SelectSingleNode("dcsset:appearance", $ns)
$ap = Get-SettingsAppearance $appNode
if ($ap -and $ap.Count -gt 0) { $entry['appearance'] = $ap }
$pres = Get-Text $it "dcsset:presentation"
if ($pres) { $entry['presentation'] = $pres }
$vm = Get-Text $it "dcsset:viewMode"
if ($vm) { $entry['viewMode'] = $vm }
$usid = Get-Text $it "dcsset:userSettingID"
if ($usid) { $entry['userSettingID'] = 'auto' }
$out += $entry
$i++
}
return ,$out
}
# Build outputParameters dict
function Build-OutputParameters {
param($opNode)
if (-not $opNode) { return $null }
$d = [ordered]@{}
foreach ($it in $opNode.SelectNodes("dcscor:item", $ns)) {
$pName = Get-Text $it "dcscor:parameter"
$val = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $pName -or -not $val) { continue }
$vType = Get-LocalXsiType $val
if ($vType -eq 'LocalStringType') { $d[$pName] = Get-MLText $val }
else { $d[$pName] = $val.InnerText }
}
return $d
}
# Build dataParameters — return "auto" if every non-hidden top-level param appears
# with userSettingID and value matches default; otherwise return explicit list.
function Build-DataParameters {
param($dpNode, $topParams)
if (-not $dpNode) { return $null }
$items = $dpNode.SelectNodes("dcscor:item", $ns)
if ($items.Count -eq 0) { return $null }
# Build a quick map name → top-level rawParam
$visibleTop = @{}
foreach ($tp in $topParams) {
if (-not $tp.hidden -and -not $script:autoDatesCompanions.ContainsKey($tp.name)) {
$visibleTop[$tp.name] = $tp
}
}
$canAuto = $true
$presentNames = @{}
$entries = @()
foreach ($it in $items) {
$pn = Get-Text $it "dcscor:parameter"
$presentNames[$pn] = $true
$usid = Get-Text $it "dcsset:userSettingID"
if (-not $usid) { $canAuto = $false }
# Compare value to top-level param value
$valNode = $it.SelectSingleNode("dcscor:value", $ns)
$use = Get-Text $it "dcsset:use"
$tp = $visibleTop[$pn]
$flags = @()
if ($usid) { $flags += '@user' }
if ($use -eq 'false') { $flags += '@off' }
$vt = Get-LocalXsiType $valNode
$vDisplay = $null
if ($vt -eq 'StandardPeriod') {
$variant = Get-Text $valNode "v8:variant"
if ($variant -and $variant -ne 'Custom') { $vDisplay = $variant }
} elseif ($vt -eq 'DesignTimeValue') {
$vDisplay = $valNode.InnerText
} elseif ($vt -eq 'LocalStringType') {
$vDisplay = Get-MLText $valNode
} else {
if ($valNode) { $vDisplay = $valNode.InnerText }
}
# Compare to top-level default
if ($tp -and $tp.valueDisplay -ne $vDisplay) { $canAuto = $false }
if (-not $tp) { $canAuto = $false } # extra param not in top-level
# Build shorthand entry
$s = $pn
if ($null -ne $vDisplay -and $vDisplay -ne '') { $s += " = $vDisplay" }
if ($flags) { $s += ' ' + ($flags -join ' ') }
$entries += $s
}
# Check that all visible top-level params are present
foreach ($vn in $visibleTop.Keys) { if (-not $presentNames.ContainsKey($vn)) { $canAuto = $false } }
if ($canAuto) { return 'auto' }
return ,$entries
}
# Read groupItems -> array of field names (with periodAddition/groupType warnings)
function Get-GroupFields {
param($parentNode, [string]$loc)
$gFields = @()
$gi = $parentNode.SelectSingleNode("dcsset:groupItems", $ns)
if (-not $gi) { return ,$gFields }
foreach ($gItem in $gi.SelectNodes("dcsset:item", $ns)) {
$gxt = Get-LocalXsiType $gItem
if ($gxt -eq 'GroupItemField') {
$gf = Get-Text $gItem "dcsset:field"
$pat = Get-Text $gItem "dcsset:periodAdditionType"
$gt = Get-Text $gItem "dcsset:groupType"
if (($pat -and $pat -ne 'None') -or ($gt -and $gt -ne 'Items')) {
$null = Add-Warning -kind 'GroupItemDetails' -loc "$loc/groupItems" -detail "Группировка $gf использует groupType=$gt, periodAdditionType=$pat — не воспроизводится в shorthand"
}
$gFields += $gf
} else {
$gFields += (New-Sentinel -kind "GroupItem:$gxt" -loc "$loc/groupItems" -detail 'Тип элемента группировки не покрыт')
}
}
return ,$gFields
}
# Read a {groupItems, order, selection} sub-block (for table column/row, chart point/series).
# Skips Auto-only order/selection (they are platform defaults).
function Build-TableAxisBlock {
param($node, [string]$loc, [bool]$includeName = $false)
$entry = [ordered]@{}
if ($includeName) {
$nm = Get-Text $node "dcsset:name"
if ($nm) { $entry['name'] = $nm }
}
$gf = Get-GroupFields -parentNode $node -loc $loc
if ($gf.Count -gt 0) { $entry['groupFields'] = $gf }
$ordNode = $node.SelectSingleNode("dcsset:order", $ns)
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
if ($ordItems.Count -gt 0 -and -not ($ordItems.Count -eq 1 -and $ordItems[0] -eq 'Auto')) {
$entry['order'] = $ordItems
}
$selNode = $node.SelectSingleNode("dcsset:selection", $ns)
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
if ($selItems.Count -gt 0 -and -not ($selItems.Count -eq 1 -and $selItems[0] -eq 'Auto')) {
$entry['selection'] = $selItems
}
return $entry
}
# Build structure recursively. Returns array of structure items (object form).
# Caller can later try to fold linear chain into string shorthand.
function Build-Structure {
param($node, [string]$loc)
if (-not $node) { return @() }
$items = @()
$idx = 0
foreach ($it in $node.SelectNodes("dcsset:item", $ns)) {
$xt = Get-LocalXsiType $it
if ($xt -eq 'StructureItemTable') {
$entry = [ordered]@{ type = 'table' }
$nm = Get-Text $it "dcsset:name"
if ($nm) { $entry['name'] = $nm }
$cols = @()
foreach ($cn in $it.SelectNodes("dcsset:column", $ns)) {
$cols += (Build-TableAxisBlock -node $cn -loc "$loc/$idx/column")
}
if ($cols.Count -gt 0) { $entry['columns'] = $cols }
$rows = @()
foreach ($rn in $it.SelectNodes("dcsset:row", $ns)) {
$rows += (Build-TableAxisBlock -node $rn -loc "$loc/$idx/row" -includeName $true)
}
if ($rows.Count -gt 0) { $entry['rows'] = $rows }
$items += $entry
$idx++
continue
}
if ($xt -eq 'StructureItemChart') {
$entry = [ordered]@{ type = 'chart' }
$nm = Get-Text $it "dcsset:name"
if ($nm) { $entry['name'] = $nm }
$pn = $it.SelectSingleNode("dcsset:point", $ns)
if ($pn) { $entry['points'] = Build-TableAxisBlock -node $pn -loc "$loc/$idx/point" }
$sn = $it.SelectSingleNode("dcsset:series", $ns)
if ($sn) { $entry['series'] = Build-TableAxisBlock -node $sn -loc "$loc/$idx/series" }
$selN = $it.SelectSingleNode("dcsset:selection", $ns)
$selI = Build-Selection -selNode $selN -loc "$loc/$idx/selection"
if ($selI.Count -gt 0 -and -not ($selI.Count -eq 1 -and $selI[0] -eq 'Auto')) {
$entry['selection'] = $selI
}
$opN = $it.SelectSingleNode("dcsset:outputParameters", $ns)
$op = Build-OutputParameters -opNode $opN
if ($op -and $op.Count -gt 0) { $entry['outputParameters'] = $op }
$items += $entry
$idx++
continue
}
if ($xt -ne 'StructureItemGroup') {
$items += (New-Sentinel -kind "StructureItem:$xt" -loc $loc -detail 'Тип структуры пока не покрыт')
$idx++
continue
}
$entry = [ordered]@{}
# Optional name
$nm = Get-Text $it "dcsset:name"
if ($nm) { $entry['name'] = $nm }
# groupItems → groupFields
$gi = $it.SelectSingleNode("dcsset:groupItems", $ns)
$gFields = @()
if ($gi) {
foreach ($gItem in $gi.SelectNodes("dcsset:item", $ns)) {
$gxt = Get-LocalXsiType $gItem
if ($gxt -eq 'GroupItemField') {
$gf = Get-Text $gItem "dcsset:field"
# Look at periodAdditionType — non-None or non-default → sentinel
$pat = Get-Text $gItem "dcsset:periodAdditionType"
$gt = Get-Text $gItem "dcsset:groupType"
if (($pat -and $pat -ne 'None') -or ($gt -and $gt -ne 'Items')) {
# Non-default grouping — record but don't fail
# We still emit the field; flag in warnings only
$null = Add-Warning -kind 'GroupItemDetails' -loc "$loc/groupItems" -detail "Группировка $gf использует groupType=$gt, periodAdditionType=$pat — не воспроизводится в shorthand"
}
$gFields += $gf
} else {
$gFields += (New-Sentinel -kind "GroupItem:$gxt" -loc "$loc/groupItems" -detail 'Тип элемента группировки не покрыт')
}
}
}
if ($gFields.Count -gt 0) { $entry['groupFields'] = $gFields }
# Local selection — only emit if not "[Auto]" default
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
if ($selItems.Count -gt 0 -and -not ($selItems.Count -eq 1 -and $selItems[0] -eq 'Auto')) {
$entry['selection'] = $selItems
}
# Local order
$ordNode = $it.SelectSingleNode("dcsset:order", $ns)
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
if ($ordItems.Count -gt 0 -and -not ($ordItems.Count -eq 1 -and $ordItems[0] -eq 'Auto')) {
$entry['order'] = $ordItems
}
# Local filter
$filterNode = $it.SelectSingleNode("dcsset:filter", $ns)
if ($filterNode -and $filterNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$f = @()
foreach ($fc in $filterNode.SelectNodes("dcsset:item", $ns)) { $f += (Build-FilterItem -itemNode $fc -loc "$loc/filter") }
$entry['filter'] = $f
}
# Children — recursive
$children = Build-Structure -node $it -loc "$loc/children"
if ($children.Count -gt 0) { $entry['children'] = $children }
$items += $entry
$idx++
}
return ,$items
}
# Try to fold a structure tree into string shorthand "A > B > details".
# Conditions: linear chain (each level has exactly one child), each level is
# a plain group with single groupField and no local selection/order/filter.
function Try-StructureShorthand {
param($items)
if ($items.Count -ne 1) { return $null }
$parts = @()
$cur = $items[0]
while ($null -ne $cur) {
# Disallow extras
if ($cur.Contains('type') -and $cur['type'] -ne 'group') { return $null }
if ($cur.Contains('name')) { return $null }
if ($cur.Contains('selection')) { return $null }
if ($cur.Contains('order')) { return $null }
if ($cur.Contains('filter')) { return $null }
$gfs = $cur['groupFields']
if ($null -eq $gfs -or $gfs.Count -eq 0) {
# details level (terminal)
$parts += 'details'
break
}
if ($gfs.Count -ne 1) { return $null }
$parts += $gfs[0]
$children = $cur['children']
if ($null -eq $children -or $children.Count -eq 0) { break }
if ($children.Count -ne 1) { return $null }
$cur = $children[0]
}
return ($parts -join ' > ')
}
# --- 4. dataSources ---
$dataSources = @()
$dsourceNodes = $root.SelectNodes("r:dataSource", $ns)
foreach ($dsn in $dsourceNodes) {
$nm = Get-Text $dsn "r:name"
$tp = Get-Text $dsn "r:dataSourceType"
$dataSources += [ordered]@{ name = $nm; type = $tp }
}
# Default: single ИсточникДанных1/Local → omit from output
$emitDataSources = $true
if ($dataSources.Count -eq 1 -and $dataSources[0].name -eq 'ИсточникДанных1' -and $dataSources[0].type -eq 'Local') {
$emitDataSources = $false
}
# --- 5. dataSets ---
function Build-DataSet {
param($dsNode, [string]$loc)
$xsiType = Get-LocalXsiType $dsNode
$name = Get-Text $dsNode "r:name"
$ds = [ordered]@{ name = $name }
switch ($xsiType) {
'DataSetQuery' {
$ds['query'] = Get-Text $dsNode "r:query"
}
'DataSetObject' {
$ds['objectName'] = Get-Text $dsNode "r:objectName"
}
'DataSetUnion' {
$nested = @()
$ni = 0
foreach ($nNode in $dsNode.SelectNodes("r:dataSet", $ns)) {
$nested += (Build-DataSet -dsNode $nNode -loc "$loc/items[$ni]")
$ni++
}
$ds['items'] = $nested
}
default {
$ds['__unsupported__'] = (New-Sentinel -kind "DataSetType:$xsiType" -loc $loc -detail "Неизвестный тип набора данных")['__unsupported__']
}
}
# Fields (Query, Object, and Union itself can all have fields)
$fieldNodes = $dsNode.SelectNodes("r:field", $ns)
if ($fieldNodes.Count -gt 0) {
$fields = @()
$fi = 0
foreach ($fn in $fieldNodes) {
$fxsi = Get-LocalXsiType $fn
if ($fxsi -ne 'DataSetFieldField') {
$fields += (New-Sentinel -kind "FieldType:$fxsi" -loc "$loc/field[$fi]" -detail 'Тип поля не DataSetFieldField')
} else {
$fields += (Build-Field -fieldNode $fn -loc "$loc/field[$fi]")
}
$fi++
}
$ds['fields'] = $fields
}
# dataSource attachment — omit if matches default (Union has no dataSource)
if ($xsiType -ne 'DataSetUnion') {
$dsSrc = Get-Text $dsNode "r:dataSource"
if ($emitDataSources -and $dsSrc) { $ds['dataSource'] = $dsSrc }
}
return $ds
}
$dataSets = @()
$dsNodes = $root.SelectNodes("r:dataSet", $ns)
$dsi = 0
foreach ($dsNode in $dsNodes) {
$dataSets += (Build-DataSet -dsNode $dsNode -loc "dataSet[$dsi]")
$dsi++
}
# --- 5b. calculatedFields ---
$calculatedFields = @()
$cfNodes = $root.SelectNodes("r:calculatedField", $ns)
$ci = 0
foreach ($cf in $cfNodes) {
$calculatedFields += (Build-CalcField -cfNode $cf -loc "calculatedField[$ci]")
$ci++
}
# --- 5c. totalFields ---
$totalFields = @()
$tfNodes = $root.SelectNodes("r:totalField", $ns)
foreach ($tf in $tfNodes) { $totalFields += (Build-TotalField -tfNode $tf) }
# --- 5d. parameters with autoDates folding ---
$script:autoDatesCompanions = @{}
$paramsRaw = @()
$pi = 0
$pNodes = $root.SelectNodes("r:parameter", $ns)
foreach ($p in $pNodes) {
$paramsRaw += (Build-Parameter -pNode $p -loc "parameter[$pi]")
$pi++
}
# Detect autoDates: for each StandardPeriod parameter P, look for two siblings with
# expression "&P.ДатаНачала" and "&P.ДатаОкончания". If both found, mark P with @autoDates
# and remove the companions.
$paramByName = @{}
foreach ($p in $paramsRaw) { $paramByName[$p.name] = $p }
$removedNames = @{}
$script:autoDatesCompanions = @{}
foreach ($p in $paramsRaw) {
if ($p.typeShort -ne 'StandardPeriod') { continue }
$parentName = $p.name
$startExpr = '&' + $parentName + '.ДатаНачала'
$endExpr = '&' + $parentName + '.ДатаОкончания'
$startMatch = $null
$endMatch = $null
foreach ($q in $paramsRaw) {
if ($q.name -eq $parentName) { continue }
if ($q.expression -eq $startExpr) { $startMatch = $q.name }
elseif ($q.expression -eq $endExpr) { $endMatch = $q.name }
}
if ($startMatch -and $endMatch) {
$p['autoDates'] = $true
$removedNames[$startMatch] = $true
$removedNames[$endMatch] = $true
$script:autoDatesCompanions[$startMatch] = $true
$script:autoDatesCompanions[$endMatch] = $true
}
}
$parameters = @()
foreach ($p in $paramsRaw) {
if ($removedNames.ContainsKey($p.name)) { continue }
$parameters += (Render-Parameter -p $p)
}
# --- 6. Build top-level JSON object ---
$out = [ordered]@{}
if ($emitDataSources) { $out['dataSources'] = $dataSources }
$out['dataSets'] = $dataSets
if ($calculatedFields.Count -gt 0) { $out['calculatedFields'] = $calculatedFields }
if ($totalFields.Count -gt 0) { $out['totalFields'] = $totalFields }
if ($parameters.Count -gt 0) { $out['parameters'] = $parameters }
# --- 5e. templates ---
$templates = @()
$tNodes = $root.SelectNodes("r:template", $ns)
$ti = 0
foreach ($tn in $tNodes) {
$templates += (Build-Template -templateNode $tn -loc "template[$ti]")
$ti++
}
if ($templates.Count -gt 0) { $out['templates'] = $templates }
# --- 5f. groupTemplates ---
$groupTemplates = @()
# <groupHeaderTemplate> → templateType = "GroupHeader"
foreach ($ght in $root.SelectNodes("r:groupHeaderTemplate", $ns)) {
$entry = [ordered]@{}
$gn = Get-Text $ght "r:groupName"
$gf = Get-Text $ght "r:groupField"
if ($gn) { $entry['groupName'] = $gn }
if ($gf) { $entry['groupField'] = $gf }
$entry['templateType'] = 'GroupHeader'
$entry['template'] = Get-Text $ght "r:template"
$groupTemplates += $entry
}
# <groupTemplate> → templateType from inner <templateType>
foreach ($gt in $root.SelectNodes("r:groupTemplate", $ns)) {
$entry = [ordered]@{}
$gn = Get-Text $gt "r:groupName"
$gf = Get-Text $gt "r:groupField"
if ($gn) { $entry['groupName'] = $gn }
if ($gf) { $entry['groupField'] = $gf }
$entry['templateType'] = Get-Text $gt "r:templateType"
$entry['template'] = Get-Text $gt "r:template"
$groupTemplates += $entry
}
if ($groupTemplates.Count -gt 0) { $out['groupTemplates'] = $groupTemplates }
# --- 5g. settingsVariants ---
$settingsVariants = @()
$svNodes = $root.SelectNodes("r:settingsVariant", $ns)
$vi = 0
foreach ($sv in $svNodes) {
$vname = Get-Text $sv "dcsset:name"
$presNode = $sv.SelectSingleNode("dcsset:presentation", $ns)
$presentation = Get-MLText $presNode
$settingsNode = $sv.SelectSingleNode("dcsset:settings", $ns)
$settings = [ordered]@{}
# selection (top-level)
$selTop = $settingsNode.SelectSingleNode("dcsset:selection", $ns)
$selItems = Build-Selection -selNode $selTop -loc "variant[$vi]/selection"
if ($selItems.Count -gt 0) { $settings['selection'] = $selItems }
# filter
$fTop = $settingsNode.SelectSingleNode("dcsset:filter", $ns)
if ($fTop -and $fTop.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$fa = @()
foreach ($fc in $fTop.SelectNodes("dcsset:item", $ns)) { $fa += (Build-FilterItem -itemNode $fc -loc "variant[$vi]/filter") }
$settings['filter'] = $fa
}
# order
$ordTop = $settingsNode.SelectSingleNode("dcsset:order", $ns)
$ordItems = Build-Order -ordNode $ordTop -loc "variant[$vi]/order"
if ($ordItems.Count -gt 0) { $settings['order'] = $ordItems }
# conditionalAppearance
$caTop = $settingsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns)
if ($caTop) {
$ca = Build-ConditionalAppearance -caNode $caTop -loc "variant[$vi]/ca"
if ($ca.Count -gt 0) { $settings['conditionalAppearance'] = $ca }
}
# outputParameters
$opTop = $settingsNode.SelectSingleNode("dcsset:outputParameters", $ns)
$op = Build-OutputParameters -opNode $opTop
if ($op -and $op.Count -gt 0) { $settings['outputParameters'] = $op }
# dataParameters
$dpTop = $settingsNode.SelectSingleNode("dcsset:dataParameters", $ns)
$dp = Build-DataParameters -dpNode $dpTop -topParams $paramsRaw
if ($null -ne $dp) { $settings['dataParameters'] = $dp }
# structure — top-level <dcsset:item> children of <dcsset:settings>
$structItems = Build-Structure -node $settingsNode -loc "variant[$vi]/structure"
if ($structItems.Count -gt 0) {
$short = Try-StructureShorthand $structItems
if ($short) { $settings['structure'] = $short }
else { $settings['structure'] = $structItems }
}
# Skip pure-default variants: settings contains only "details" structure (or nothing) +
# name=Основной + no distinctive title.
$nonStructKeys = @($settings.Keys | Where-Object { $_ -ne 'structure' })
$structOnlyDetails = (-not $settings.Contains('structure')) -or ($settings['structure'] -eq 'details')
$isDefault = ($nonStructKeys.Count -eq 0) -and $structOnlyDetails -and ($vname -eq 'Основной') -and (-not $presentation -or $presentation -eq $vname)
if (-not $isDefault) {
$entry = [ordered]@{ name = $vname }
if ($presentation -and $presentation -ne $vname) { $entry['title'] = $presentation }
$entry['settings'] = $settings
$settingsVariants += $entry
}
$vi++
}
if ($settingsVariants.Count -gt 0) { $out['settingsVariants'] = $settingsVariants }
# --- 7. Serialize ---
$json = $out | ConvertTo-Json -Depth 32
# Unescape \uXXXX → UTF-8 literals
$json = [regex]::Replace($json, '\\u([0-9a-fA-F]{4})', {
param($m)
[char][int]("0x" + $m.Groups[1].Value)
})
if ($OutputPath) {
if (-not [System.IO.Path]::IsPathRooted($OutputPath)) {
$OutputPath = Join-Path (Get-Location).Path $OutputPath
}
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($OutputPath, $json, $enc)
if ($script:warnings.Count -gt 0) {
$wPath = [System.IO.Path]::ChangeExtension($OutputPath, $null).TrimEnd('.') + '.warnings.md'
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("# skd-decompile warnings")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("Source: $TemplatePath")
[void]$sb.AppendLine("")
foreach ($w in $script:warnings) {
$wId = $w.id; $wKind = $w.kind; $wLoc = $w.loc; $wDetail = $w.detail
[void]$sb.AppendLine("- **$wId** ($wKind) at $wLoc$wDetail")
}
[System.IO.File]::WriteAllText($wPath, $sb.ToString(), $enc)
Write-Host "Warnings: $wPath ($($script:warnings.Count) issue(s))" -ForegroundColor Yellow
}
[Console]::Error.WriteLine("Decompiled: dataSets=$($dataSets.Count), calc=$($calculatedFields.Count), totals=$($totalFields.Count), params=$($parameters.Count), templates=$($templates.Count), groupTemplates=$($groupTemplates.Count), variants=$($settingsVariants.Count), warnings=$($script:warnings.Count)")
} else {
Write-Output $json
if ($script:warnings.Count -gt 0) {
[Console]::Error.WriteLine("Warnings ($($script:warnings.Count)):")
foreach ($w in $script:warnings) {
[Console]::Error.WriteLine(" $($w.id) [$($w.kind)] $($w.loc): $($w.detail)")
}
}
}