Files
cc-1c-skills/.claude/skills/skd-decompile/scripts/skd-decompile.ps1
T
Nick Shirokov 9b4bb3d9b8 feat(skd): dataSetLinks с расширенными атрибутами
skd-decompile теперь извлекает блоки <dataSetLink> на уровне схемы,
skd-compile поддерживает поля parameterListAllowed/startExpression/
linkConditionExpression (раньше был только parameter).

На sample30 это даёт −1100 строк diff (4873 → 3773), один отчёт
(АнализНачисленийНДССАвансовПолученных) переходит в bit-perfect.
2026-05-23 12:47:56 +03:00

2460 lines
96 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.
# skd-decompile v0.47 — 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"
}
# nestedSchema — DCS внутри DCS со своими dataSet/parameters/templates
foreach ($ns_el in $xmlDoc.SelectNodes("//*[local-name()='nestedSchema']")) {
Fail-Ring3 -kind "nestedSchema (вложенные подсхемы)" -loc "nestedSchema"
}
# --- 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 ---
# Custom JSON serializer — компактный, 2-пробельный indent, массивы примитивов inline.
# В отличие от ConvertTo-Json (PS5.1):
# - не выравнивает ключи объекта по самому длинному
# - не разворачивает массивы примитивов на отдельные строки
# - кириллица в UTF-8 (без \uXXXX-escapes)
function Convert-StringToJsonLiteral {
param([string]$s)
if ($null -eq $s) { return 'null' }
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append('"')
foreach ($ch in $s.ToCharArray()) {
$code = [int]$ch
if ($code -eq 0x22) { [void]$sb.Append('\"') }
elseif ($code -eq 0x5C) { [void]$sb.Append('\\') }
elseif ($code -eq 0x08) { [void]$sb.Append('\b') }
elseif ($code -eq 0x09) { [void]$sb.Append('\t') }
elseif ($code -eq 0x0A) { [void]$sb.Append('\n') }
elseif ($code -eq 0x0C) { [void]$sb.Append('\f') }
elseif ($code -eq 0x0D) { [void]$sb.Append('\r') }
elseif ($code -lt 0x20) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
else { [void]$sb.Append($ch) }
}
[void]$sb.Append('"')
return $sb.ToString()
}
# Попробовать сериализовать значение полностью inline (одна строка).
# Возвращает строку либо $null, если содержимое не помещается.
function Try-InlineJson {
param($obj)
if ($null -eq $obj) { return 'null' }
if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
}
if ($obj -is [System.Collections.IDictionary]) {
if ($obj.Count -eq 0) { return '{}' }
$parts = @()
foreach ($k in $obj.Keys) {
$v = Try-InlineJson $obj[$k]
if ($null -eq $v) { return $null }
$parts += "$(Convert-StringToJsonLiteral "$k"): $v"
}
return '{ ' + ($parts -join ', ') + ' }'
}
if ($obj -is [System.Management.Automation.PSCustomObject]) {
$props = @($obj.PSObject.Properties)
if ($props.Count -eq 0) { return '{}' }
$parts = @()
foreach ($p in $props) {
$v = Try-InlineJson $p.Value
if ($null -eq $v) { return $null }
$parts += "$(Convert-StringToJsonLiteral "$($p.Name)"): $v"
}
return '{ ' + ($parts -join ', ') + ' }'
}
if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
$items = @($obj)
if ($items.Count -eq 0) { return '[]' }
$parts = @()
foreach ($it in $items) {
$v = Try-InlineJson $it
if ($null -eq $v) { return $null }
$parts += $v
}
return '[' + ($parts -join ', ') + ']'
}
return $null
}
function ConvertTo-CompactJson {
param($obj, [int]$depth = 0, [string]$indentUnit = ' ', [int]$lineLimit = 400)
$indent = $indentUnit * $depth
$childIndent = $indentUnit * ($depth + 1)
if ($null -eq $obj) { return 'null' }
if ($obj -is [bool]) { if ($obj) { return 'true' } else { return 'false' } }
if ($obj -is [string]) { return (Convert-StringToJsonLiteral $obj) }
if ($obj -is [int] -or $obj -is [long]) { return "$obj" }
if ($obj -is [double] -or $obj -is [single] -or $obj -is [decimal]) {
return ([System.Convert]::ToString($obj, [System.Globalization.CultureInfo]::InvariantCulture))
}
# Try inline для объектов и массивов с объектами — если помещается в lineLimit с учётом текущего indent.
$isContainer = ($obj -is [System.Collections.IDictionary]) -or ($obj -is [System.Management.Automation.PSCustomObject]) -or ($obj -is [array]) -or ($obj -is [System.Collections.IList])
if ($isContainer) {
$inlineAttempt = Try-InlineJson $obj
if ($null -ne $inlineAttempt -and ($indent.Length + $inlineAttempt.Length) -le $lineLimit) {
return $inlineAttempt
}
}
# Hashtable / OrderedDictionary — объект multi-line
if ($obj -is [System.Collections.IDictionary]) {
$keys = @($obj.Keys)
if ($keys.Count -eq 0) { return '{}' }
$parts = @()
foreach ($k in $keys) {
$val = ConvertTo-CompactJson -obj $obj[$k] -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit
$parts += "$childIndent$(Convert-StringToJsonLiteral "$k"): $val"
}
return "{`n" + ($parts -join ",`n") + "`n$indent}"
}
if ($obj -is [System.Management.Automation.PSCustomObject]) {
$props = @($obj.PSObject.Properties)
if ($props.Count -eq 0) { return '{}' }
$parts = @()
foreach ($p in $props) {
$val = ConvertTo-CompactJson -obj $p.Value -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit
$parts += "$childIndent$(Convert-StringToJsonLiteral "$($p.Name)"): $val"
}
return "{`n" + ($parts -join ",`n") + "`n$indent}"
}
# Array / IList multi-line
if ($obj -is [array] -or $obj -is [System.Collections.IList]) {
$items = @($obj)
if ($items.Count -eq 0) { return '[]' }
$parts = @($items | ForEach-Object { "$childIndent$(ConvertTo-CompactJson -obj $_ -depth ($depth + 1) -indentUnit $indentUnit -lineLimit $lineLimit)" })
return "[`n" + ($parts -join ",`n") + "`n$indent]"
}
# Fallback
return (Convert-StringToJsonLiteral "$obj")
}
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> → @{ tokens, extras }
# tokens — список @-флагов (boolean dcscom children); @period — sugar для periodNumber=1+periodType=Main
# extras — любые dcscom:KEY со строковым значением (balanceGroupName/balanceType/parentDimension/...).
# compile/skd-edit принимают произвольные KV — никакого whitelist'а.
function Get-RoleInfo {
param($roleNode, [string]$loc)
if (-not $roleNode) { return $null }
$tokens = @()
$extras = [ordered]@{}
$hasComplex = $false
# Сначала проверяем @period sugar: periodNumber=1 + periodType=Main
$pnNode = $roleNode.SelectSingleNode("dcscom:periodNumber", $ns)
$ptNode = $roleNode.SelectSingleNode("dcscom:periodType", $ns)
$periodHandled = $false
if ($pnNode -and $ptNode -and $pnNode.InnerText -eq '1' -and $ptNode.InnerText -eq 'Main') {
$tokens += '@period'
$periodHandled = $true
}
foreach ($child in $roleNode.ChildNodes) {
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if ($child.NamespaceURI -ne $NS_COM) { $hasComplex = $true; continue }
# Skip periodNumber/periodType if уже свернули в @period
if ($periodHandled -and ($child.LocalName -eq 'periodNumber' -or $child.LocalName -eq 'periodType')) { continue }
$txt = $child.InnerText
if ($txt -eq 'true') {
$tokens += '@' + $child.LocalName
} elseif ($txt -eq 'false' -or -not $txt) {
# Игнорируем явный false (дефолт)
} else {
# Любая строка → extra (без whitelist — compile эмитит любой ключ)
$extras[$child.LocalName] = $txt
}
}
if ($hasComplex) {
$null = New-Sentinel -kind 'ComplexRole' -loc $loc -detail 'Роль с не-dcscom-атрибутами не сворачивается в DSL'
}
return [ordered]@{ tokens = $tokens; extras = $extras }
}
# Render role into shorthand string (если все extras "простые") или object form.
# Returns hashtable @{ value = <string|object|array>; isString = $true|$false } or $null если роль пустая.
function Render-Role {
param($tokens, $extras)
$hasExtras = $extras -and $extras.Count -gt 0
$hasTokens = $tokens -and $tokens.Count -gt 0
if (-not $hasExtras -and -not $hasTokens) { return $null }
if (-not $hasExtras) {
# Только флаги: одиночный — без @ (back-compat), множественный — "@a @b" string.
$plain = @($tokens | ForEach-Object { $_ -replace '^@','' })
if ($plain.Count -eq 1) { return @{ value = $plain[0]; isString = $true } }
$withAt = @($plain | ForEach-Object { "@$_" })
return @{ value = ($withAt -join ' '); isString = $true }
}
# Есть extras — проверяем, все ли значения "простые" (без пробелов и кавычек)
$allSimple = $true
foreach ($v in $extras.Values) {
if ("$v" -notmatch '^[\w\.\-]+$') { $allSimple = $false; break }
}
if ($allSimple) {
# Shorthand: "@flag1 @flag2 K=V K=V"
$parts = @()
foreach ($t in $tokens) { $parts += $t }
foreach ($k in $extras.Keys) { $parts += "$k=$($extras[$k])" }
return @{ value = ($parts -join ' '); isString = $true }
}
# Object form
$obj = [ordered]@{}
foreach ($t in $tokens) { $obj[($t -replace '^@','')] = $true }
foreach ($k in $extras.Keys) { $obj[$k] = $extras[$k] }
return @{ value = $obj; isString = $false }
}
# <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') {
$rawVal = Get-MLText $valNode
} else {
$rawVal = $valNode.InnerText
}
# <dcscor:use>false</...> → wrapper {value, use: false}
$useV = Get-Text $it "dcscor:use"
if ($useV -eq 'false') {
$dict[$p] = [ordered]@{ value = $rawVal; use = $false }
} else {
$dict[$p] = $rawVal
}
}
return $dict
}
# Read <r:inputParameters> → JSON array. Returns $null если отсутствует или пустой.
function Read-InputParameters {
param($parentNode)
$ip = $parentNode.SelectSingleNode("r:inputParameters", $ns)
if (-not $ip) { return $null }
$result = @()
foreach ($it in $ip.SelectNodes("dcscor:item", $ns)) {
$entry = [ordered]@{}
$useText = Get-Text $it "dcscor:use"
$pName = Get-Text $it "dcscor:parameter"
$entry['parameter'] = $pName
if ($useText -eq 'false') { $entry['use'] = $false }
$val = $it.SelectSingleNode("dcscor:value", $ns)
if ($val) {
$vType = Get-LocalXsiType $val
if ($vType -eq 'ChoiceParameters') {
$cp = @()
foreach ($cpItem in $val.SelectNodes("dcscor:item", $ns)) {
$cpEntry = [ordered]@{ name = Get-Text $cpItem "dcscor:choiceParameter" }
$values = @()
foreach ($v in $cpItem.SelectNodes("dcscor:value", $ns)) { $values += $v.InnerText }
$cpEntry['values'] = $values
$cp += $cpEntry
}
$entry['choiceParameters'] = $cp
} elseif ($vType -eq 'ChoiceParameterLinks') {
$cpl = @()
foreach ($cplItem in $val.SelectNodes("dcscor:item", $ns)) {
$cplEntry = [ordered]@{
name = Get-Text $cplItem "dcscor:choiceParameter"
value = Get-Text $cplItem "dcscor:value"
}
$mode = Get-Text $cplItem "dcscor:mode"
if ($mode) { $cplEntry['mode'] = $mode }
$cpl += $cplEntry
}
$entry['choiceParameterLinks'] = $cpl
} elseif ($vType -eq 'LocalStringType') {
# Multilang dict {ru, en, ...}
$ml = Get-MLText $val
if ($ml) { $entry['value'] = $ml } else { $entry['value'] = '' }
} else {
# Simple typed value
$txt = $val.InnerText
if ($vType -eq 'boolean') {
$entry['value'] = ($txt -eq 'true')
} elseif ($vType -eq 'decimal') {
if ($txt -match '^-?\d+$') { $entry['value'] = [int]$txt }
else { $entry['value'] = [double]$txt }
} else {
$entry['value'] = $txt
}
}
}
$result += $entry
}
if ($result.Count -eq 0) { return $null }
return ,$result
}
# Build a field JSON entry (shorthand if possible, object form otherwise)
function Build-Field {
param($fieldNode, [string]$loc)
# inputParameters теперь поддерживается в DSL — читается ниже в needsObject
$inputParameters = Read-InputParameters -parentNode $fieldNode
# orderExpression теперь поддерживается в DSL — читается ниже в needsObject
$orderExprNode = $fieldNode.SelectSingleNode("r:orderExpression", $ns)
$orderExpression = $null
if ($orderExprNode) {
$oeExpr = Get-Text $orderExprNode "dcscom:expression"
$oeType = Get-Text $orderExprNode "dcscom:orderType"
$oeAuto = Get-Text $orderExprNode "dcscom:autoOrder"
$orderExpression = [ordered]@{}
if ($oeExpr) { $orderExpression['expression'] = $oeExpr }
if ($oeType) { $orderExpression['orderType'] = $oeType }
# autoOrder=false — это дефолт; emit'им только если true (или явно записан false)
if ($oeAuto -eq 'true') { $orderExpression['autoOrder'] = $true }
elseif ($oeAuto -eq 'false') { $orderExpression['autoOrder'] = $false }
}
$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
$roleInfo = Get-RoleInfo $fieldNode.SelectSingleNode("r:role", $ns) "$loc/role"
$roleTokens = if ($roleInfo) { $roleInfo.tokens } else { @() }
$roleExtras = if ($roleInfo) { $roleInfo.extras } else { [ordered]@{} }
$roleRendered = Render-Role -tokens $roleTokens -extras $roleExtras
$restrictTokens = Get-RestrictionTokens $fieldNode.SelectSingleNode("r:useRestriction", $ns)
# <attributeUseRestriction> — те же 4 флага, но для атрибутов ссылочного поля
$attrRestrictTokens = Get-RestrictionTokens $fieldNode.SelectSingleNode("r:attributeUseRestriction", $ns)
$appNode = $fieldNode.SelectSingleNode("r:appearance", $ns)
$appearance = Get-AppearanceDict $appNode
$presExpr = Get-Text $fieldNode "r:presentationExpression"
# availableValues on dataset field
$avNodes = $fieldNode.SelectNodes("r:availableValue", $ns)
$availableValues = @()
foreach ($av in $avNodes) {
$avVN = $av.SelectSingleNode("r:value", $ns)
$avPN = $av.SelectSingleNode("r:presentation", $ns)
$avEntry = [ordered]@{}
if ($avVN) {
$avType = Get-LocalXsiType $avVN
$avText = $avVN.InnerText
if ($avType -eq 'boolean') { $avEntry['value'] = ($avText -eq 'true') }
elseif ($avType -eq 'decimal') {
if ($avText -match '^-?\d+$') { $avEntry['value'] = [int]$avText }
else { $avEntry['value'] = [double]$avText }
}
else { $avEntry['value'] = $avText }
}
if ($avPN) {
$avPres = Get-MLText $avPN
if ($avPres) { $avEntry['presentation'] = $avPres }
}
$availableValues += $avEntry
}
# Можно ли роль положить в shorthand-строку?
$roleInString = $roleRendered -and $roleRendered.isString
$needsObject = $title -or $appearance -or $presExpr -or ($typeShort -is [array]) -or ($roleRendered -and -not $roleInString) -or $orderExpression -or $inputParameters -or ($availableValues.Count -gt 0) -or ($attrRestrictTokens -and $attrRestrictTokens.Count -gt 0)
if (-not $needsObject) {
# shorthand: "Name: type @role K=V #restrict"
$s = $fieldName
if ($typeShort) { $s = "$fieldName`: $typeShort" }
if ($roleInString) {
# Если значение — одиночный флаг (без @ и без =) — добавляем как @flag.
# Если уже содержит @ или K=V — добавляем как есть.
$rv = $roleRendered.value
if ($rv -match '@' -or $rv -match '=' -or $rv -match '\s') {
$s += ' ' + $rv
} else {
$s += " @$rv"
}
}
if ($restrictTokens) { $s += ' ' + ($restrictTokens -join ' ') }
# dataPath ≠ field — fall back to object form
if (-not ($dataPath -and $dataPath -ne $fieldName)) {
return $s
}
}
$obj = [ordered]@{ field = $fieldName }
if ($dataPath -and $dataPath -ne $fieldName) { $obj['dataPath'] = $dataPath }
if ($title) { $obj['title'] = $title }
if ($typeShort) { $obj['type'] = $typeShort }
if ($roleRendered) { $obj['role'] = $roleRendered.value }
if ($orderExpression) { $obj['orderExpression'] = $orderExpression }
if ($inputParameters) { $obj['inputParameters'] = $inputParameters }
if ($restrictTokens) { $obj['restrict'] = ($restrictTokens | ForEach-Object { $_ -replace '^#','' }) }
if ($attrRestrictTokens -and $attrRestrictTokens.Count -gt 0) {
$obj['attrRestrict'] = ($attrRestrictTokens | ForEach-Object { $_ -replace '^#','' })
}
if ($presExpr) { $obj['presentationExpression'] = $presExpr }
if ($availableValues.Count -gt 0) { $obj['availableValues'] = $availableValues }
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"
$groupNodes = $tfNode.SelectNodes("r:group", $ns)
$hasGroups = $groupNodes -and $groupNodes.Count -gt 0
# Object form — только если есть group или expression многострочный
if ($hasGroups -or ($expression -match "[`r`n]")) {
$obj = [ordered]@{ dataPath = $dataPath; expression = $expression }
if ($hasGroups) {
$groups = @()
foreach ($g in $groupNodes) { $groups += $g.InnerText }
$obj['group'] = $groups
}
return $obj
}
# Shorthand: "Func(dataPath)" → "name: Func" (агрегат с очевидным аргументом)
if ($expression -match '^(\w+)\((\w+)\)$' -and $matches[2] -eq $dataPath) {
return "$dataPath`: $($matches[1])"
}
# Любой другой однострочный expression → "name: expression" (compile берёт всё после ":" как expression)
return "$dataPath`: $expression"
}
# 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"
$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"
$inputParameters = Read-InputParameters -parentNode $pNode
# hidden — combo: availableAsField=false + useRestriction=true (как эмитит compile @hidden)
$notAField = ($availableAsField -eq 'false')
$hidden = $notAField -and $useRestriction
# 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
notAField = ($notAField -and -not $hidden) # availableAsField=false без useRestriction
denyIncomplete = $denyIncomplete
useAttr = $useAttr
useRestriction = $useRestriction
expression = $expression
availableValues = $availableValues
inputParameters = $inputParameters
}
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 ($p.inputParameters) { $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 ($p.notAField) { $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.notAField) { $obj['availableAsField'] = $false }
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 }
if ($p.inputParameters) { $obj['inputParameters'] = $p.inputParameters }
return $obj
}
# --- 3b. Built-in style presets (preset-shape: 11 полей) ---
# Имена 5 встроенных стилей. Совпадает с compile presets.
$script:builtinPresetNames = @('none','data','header','subheader','total')
# Преобразовать compile-style preset hashtable в наш canonical preset shape.
# Canonical поля: font, fontSize, bold, italic, hAlign, vAlign, wrap, bgColor, textColor, borderColor, borders.
$script:builtinPresets = @{
'none' = @{
font = $null; fontSize = $null; bold = $false; italic = $false
hAlign = $null; vAlign = $null; wrap = $false
bgColor = $null; textColor = $null
borderColor = $null; borders = $false
}
'data' = @{
font = 'Arial'; fontSize = 10; bold = $false; italic = $false
hAlign = $null; vAlign = $null; wrap = $false
bgColor = 'style:ReportGroup1BackColor'; textColor = $null
borderColor = 'style:ReportLineColor'; borders = $true
}
'header' = @{
font = 'Arial'; fontSize = 10; bold = $false; italic = $false
hAlign = 'Center'; vAlign = $null; wrap = $true
bgColor = 'style:ReportHeaderBackColor'; textColor = $null
borderColor = 'style:ReportLineColor'; borders = $true
}
'subheader' = @{
font = 'Arial'; fontSize = 10; bold = $false; italic = $false
hAlign = 'Center'; vAlign = $null; wrap = $true
bgColor = $null; textColor = $null
borderColor = 'style:ReportLineColor'; borders = $true
}
'total' = @{
font = 'Arial'; fontSize = 10; bold = $false; italic = $false
hAlign = $null; vAlign = $null; wrap = $false
bgColor = $null; textColor = $null
borderColor = 'style:ReportLineColor'; borders = $true
}
}
# effectivePresets = built-in + любые user-переопределения, загруженные из skd-styles.json
$script:effectivePresets = @{}
foreach ($k in $script:builtinPresets.Keys) {
$copy = @{}
foreach ($f in $script:builtinPresets[$k].Keys) { $copy[$f] = $script:builtinPresets[$k][$f] }
$script:effectivePresets[$k] = $copy
}
# existingUserPresetsRaw — копия загруженного skd-styles.json (PSCustomObject) для merge при записи.
$script:existingUserPresetsRaw = $null
# Аккумулятор внешних SQL-файлов для записи рядом с outputPath: @{name = "filename.sql"; text = "...sql text..."}
$script:queryFilesAccumulator = @()
$script:queryFileNamesUsed = @{}
# Если запрос ≥3 строк и есть outputPath — вынести в отдельный
# `<outputBasename>-<datasetName>.sql` (префикс защищает от коллизий имён при batch-decompile).
# Иначе — оставить inline.
function Maybe-ExternalizeQuery {
param([string]$queryText, [string]$datasetName)
if (-not $queryText) { return $queryText }
if (-not $script:outputDir) { return $queryText }
# Считаем строки — \r\n или \n
$lineCount = ([regex]::Matches($queryText, "`n")).Count + 1
if ($lineCount -lt 3) { return $queryText }
# Уникализация имени файла: prefix = basename outputPath (без расширения)
$safeDs = ($datasetName -replace '[^\w\-]', '_')
if (-not $safeDs) { $safeDs = 'query' }
$prefix = if ($script:outputBasename) { "$($script:outputBasename)-" } else { '' }
$fileName = "$prefix$safeDs.sql"
$suffix = 1
while ($script:queryFileNamesUsed.ContainsKey($fileName)) {
$suffix++
$fileName = "$prefix$safeDs`_$suffix.sql"
}
$script:queryFileNamesUsed[$fileName] = $true
$script:queryFilesAccumulator += [ordered]@{ fileName = $fileName; text = $queryText }
return "@$fileName"
}
# Записать все накопленные .sql файлы рядом с outputPath.
function Save-QueryFiles {
if ($script:queryFilesAccumulator.Count -eq 0) { return }
if (-not $script:outputDir) { return }
$enc = New-Object System.Text.UTF8Encoding($false)
foreach ($qf in $script:queryFilesAccumulator) {
$path = Join-Path $script:outputDir $qf.fileName
[System.IO.File]::WriteAllText($path, $qf.text, $enc)
}
[Console]::Error.WriteLine("Saved $($script:queryFilesAccumulator.Count) external query file(s)")
}
# customStylesAccumulator — новые customN, накопленные в текущем прогоне, для записи в skd-styles.json.
$script:customStylesAccumulator = [ordered]@{}
# Счётчик customN
$script:customStyleCounter = 0
# Normalize color value: 'd8p1:ReportHeaderBackColor' → 'style:ReportHeaderBackColor'
function Normalize-Color {
param($valNode)
if (-not $valNode) { return $null }
$txt = $valNode.InnerText
if ($txt -match '^d\d+p\d+:(.+)$') { return 'style:' + $matches[1] }
return $txt
}
# Build preset hashtable (11 полей) из <dcsat:appearance>.
# Возвращает $null если у ячейки нет ни одного стилевого атрибута (только per-cell).
function Extract-CellPreset {
param($appNode)
if (-not $appNode) { return $null }
$preset = @{
font = $null; fontSize = $null; bold = $false; italic = $false
hAlign = $null; vAlign = $null; wrap = $false
bgColor = $null; textColor = $null
borderColor = $null; borders = $false
}
$hasAnyStyle = $false
foreach ($it in $appNode.SelectNodes("dcscor:item", $ns)) {
$pName = Get-Text $it "dcscor:parameter"
$val = $it.SelectSingleNode("dcscor:value", $ns)
if (-not $pName) { continue }
if ($pName -in @('МинимальнаяШирина','МаксимальнаяШирина','МинимальнаяВысота','ОбъединятьПоВертикали','ОбъединятьПоГоризонтали','Расшифровка')) { continue }
switch ($pName) {
'Шрифт' {
if ($val) {
$preset.font = $val.GetAttribute("faceName")
$h = $val.GetAttribute("height")
if ($h) { $preset.fontSize = [int]$h }
$preset.bold = ($val.GetAttribute("bold") -eq 'true')
$preset.italic = ($val.GetAttribute("italic") -eq 'true')
$hasAnyStyle = $true
}
}
'ЦветФона' { if ($val) { $preset.bgColor = Normalize-Color $val; $hasAnyStyle = $true } }
'ЦветТекста' { if ($val) { $preset.textColor = Normalize-Color $val; $hasAnyStyle = $true } }
'ЦветГраницы' { if ($val) { $preset.borderColor = Normalize-Color $val; $hasAnyStyle = $true } }
'СтильГраницы' {
# borders = true если есть sub-items для 4 сторон со style=Solid
$sidesFound = 0
foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
$subName = Get-Text $sub "dcscor:parameter"
if ($subName -match '^СтильГраницы\.(Слева|Сверху|Справа|Снизу)$') { $sidesFound++ }
}
if ($sidesFound -gt 0) { $preset.borders = $true; $hasAnyStyle = $true }
}
'ГоризонтальноеПоложение' { if ($val) { $preset.hAlign = $val.InnerText; $hasAnyStyle = $true } }
'ВертикальноеПоложение' { if ($val) { $preset.vAlign = $val.InnerText; $hasAnyStyle = $true } }
'Размещение' { if ($val -and $val.InnerText -eq 'Wrap') { $preset.wrap = $true; $hasAnyStyle = $true } }
}
}
if (-not $hasAnyStyle) { return $null }
return $preset
}
# Deep-equality двух preset hashtables (11 полей).
function Compare-Preset {
param($a, $b)
foreach ($key in @('font','fontSize','bold','italic','hAlign','vAlign','wrap','bgColor','textColor','borderColor','borders')) {
if ($a[$key] -ne $b[$key]) { return $false }
}
return $true
}
# Найти имя preset'а в effectivePresets по shape. Возвращает имя или $null.
function Match-PresetByShape {
param($cellPreset)
if (-not $cellPreset) { return $null }
foreach ($name in $script:effectivePresets.Keys) {
if (Compare-Preset $cellPreset $script:effectivePresets[$name]) { return $name }
}
return $null
}
# Аллокация customN для нового, не-matched preset'а. Регистрирует в effectivePresets+accumulator.
function Allocate-CustomStyle {
param($cellPreset)
# Поиск свободного customN
$script:customStyleCounter++
$name = "custom$($script:customStyleCounter)"
while ($script:effectivePresets.ContainsKey($name)) {
$script:customStyleCounter++
$name = "custom$($script:customStyleCounter)"
}
$script:effectivePresets[$name] = $cellPreset
$script:customStylesAccumulator[$name] = $cellPreset
return $name
}
# Загрузка skd-styles.json рядом с outputPath (если есть) и наслоение на effectivePresets.
function Load-UserStyles {
param([string]$dirPath)
if (-not $dirPath) { return }
$stylesPath = Join-Path $dirPath 'skd-styles.json'
if (-not (Test-Path $stylesPath)) { return }
$raw = Get-Content -Raw -Encoding UTF8 $stylesPath | ConvertFrom-Json
$script:existingUserPresetsRaw = $raw
foreach ($prop in $raw.PSObject.Properties) {
# Compile-логика: data defaults → built-in if name match → user keys
$preset = @{}
foreach ($k in $script:builtinPresets['data'].Keys) { $preset[$k] = $script:builtinPresets['data'][$k] }
if ($script:builtinPresets.ContainsKey($prop.Name)) {
foreach ($k in $script:builtinPresets[$prop.Name].Keys) { $preset[$k] = $script:builtinPresets[$prop.Name][$k] }
}
foreach ($up in $prop.Value.PSObject.Properties) {
$preset[$up.Name] = $up.Value
}
$script:effectivePresets[$prop.Name] = $preset
}
}
# Запись skd-styles.json: preserved existing user presets + новые customN.
function Save-UserStyles {
param([string]$dirPath)
if (-not $dirPath) { return }
if ($script:customStylesAccumulator.Count -eq 0 -and -not $script:existingUserPresetsRaw) { return }
$stylesPath = Join-Path $dirPath 'skd-styles.json'
$out = [ordered]@{}
# Сначала existing (preserve порядок и значения)
if ($script:existingUserPresetsRaw) {
foreach ($prop in $script:existingUserPresetsRaw.PSObject.Properties) {
$out[$prop.Name] = $prop.Value
}
}
# Потом новые customN
foreach ($name in $script:customStylesAccumulator.Keys) {
if ($out.Contains($name)) { continue }
$out[$name] = $script:customStylesAccumulator[$name]
}
if ($out.Count -eq 0) { return }
$json = ConvertTo-CompactJson -obj $out
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($stylesPath, $json, $enc)
[Console]::Error.WriteLine("Saved skd-styles.json (custom styles: $($script:customStylesAccumulator.Count))")
}
# 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
$cellStyleMap = @{} # "r,c" → имя стиля для конкретной ячейки (null для merge/no-style)
$hasAnyStyledCell = $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 merge cells)
if ($appNode -and -not $perCell.mergeV -and -not $perCell.mergeH) {
$cellPreset = Extract-CellPreset $appNode
if ($null -ne $cellPreset) {
$matched = Match-PresetByShape $cellPreset
if ($null -eq $matched) {
$matched = Allocate-CustomStyle $cellPreset
}
$cellStyleMap["$rowIdx,$colIdx"] = $matched
$hasAnyStyledCell = $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 default = наиболее частый стиль ячеек.
$templateDefault = $null
if ($hasAnyStyledCell) {
$counts = @{}
foreach ($k in $cellStyleMap.Keys) {
$name = $cellStyleMap[$k]
if (-not $counts.ContainsKey($name)) { $counts[$name] = 0 }
$counts[$name]++
}
$maxCount = 0
foreach ($name in $counts.Keys) {
if ($counts[$name] -gt $maxCount) {
$maxCount = $counts[$name]
$templateDefault = $name
}
}
}
# Если есть ячейки со стилем, отличным от template default — оборачиваем их в object form.
if ($templateDefault) {
$rowsOut = @()
for ($r = 0; $r -lt $rows.Count; $r++) {
$newRow = @()
for ($c = 0; $c -lt $rows[$r].Count; $c++) {
$key = "$r,$c"
if ($cellStyleMap.ContainsKey($key) -and $cellStyleMap[$key] -ne $templateDefault) {
$newRow += [ordered]@{ value = $rows[$r][$c]; style = $cellStyleMap[$key] }
} else {
$newRow += $rows[$r][$c]
}
}
$rowsOut += ,$newRow
}
$rows = $rowsOut
}
# 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 ($templateDefault) {
$tmplObj['style'] = $templateDefault
} elseif ($rows.Count -gt 0) {
# Все ячейки без стилевых атрибутов — это шаблон "без стиля"
$tmplObj['style'] = 'none'
}
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
}
# Same as Get-FilterValue, но дополнительно возвращает xsi:type значения,
# чтобы caller мог сохранить valueType (например, dcscor:Field — для field-to-field
# comparison). Format: @{ value = ...; type = '<xsi-type-or-null>' }.
function Get-FilterValueWithType {
param($valNode)
if (-not $valNode) { return @{ value = '_'; type = $null } }
$rawType = $valNode.GetAttribute("type", $NS_XSI)
$nil = $valNode.GetAttribute("nil", $NS_XSI)
if ($nil -eq 'true') { return @{ value = '_'; type = $null } }
$vType = Get-LocalXsiType $valNode
if ($vType -eq 'LocalStringType') {
return @{ value = (Get-MLText $valNode); type = $rawType }
}
$txt = $valNode.InnerText
if (-not $txt) { return @{ value = '_'; type = $rawType } }
return @{ value = $txt; type = $rawType }
}
# 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")
}
$gObj = [ordered]@{ group = $groupName; items = $items }
$gPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
if ($gPresNode) {
$gPres = Get-MLText $gPresNode
if (-not $gPres) { $gPres = $gPresNode.InnerText }
if ($gPres) { $gObj['presentation'] = $gPres }
}
# viewMode: сохраняем даже Normal если node присутствует (для bit-perfect)
$gVMNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
if ($gVMNode) { $gObj['viewMode'] = $gVMNode.InnerText }
$gUSID = Get-Text $itemNode "dcsset:userSettingID"
if ($gUSID) { $gObj['userSettingID'] = 'auto' }
$gUSPN = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
if ($gUSPN) {
$gUSP = Get-MLText $gUSPN
if ($gUSP) { $gObj['userSettingPresentation'] = $gUSP }
}
return $gObj
}
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 }
# Чтение <right>: один, несколько (InList multi-value) или ValueListType (пустой list-placeholder)
$rightNodes = @($itemNode.SelectNodes("dcsset:right", $ns))
$value = $null
$valueIsArrayFlag = $false
$valueTypeAttr = $null # явный xsi:type, если не дефолтный (например, dcscor:Field)
if ($rightNodes.Count -eq 1) {
$rn = $rightNodes[0]
if ((Get-LocalXsiType $rn) -eq 'ValueListType') {
# Пустой список-placeholder для пользовательских настроек InList
$value = @()
$valueIsArrayFlag = $true
} else {
$vt = Get-FilterValueWithType $rn
$value = $vt.value
# Сохраняем тип только если он не дефолтный (auto-detect compile вернёт xs:*)
if ($vt.type -and $vt.type -notmatch '^xs:') {
$valueTypeAttr = $vt.type
}
}
} elseif ($rightNodes.Count -gt 1) {
# Несколько значений → массив (InList с конкретными значениями)
$arr = @()
foreach ($rn in $rightNodes) { $arr += (Get-FilterValue $rn) }
$value = $arr
$valueIsArrayFlag = $true
}
$use = Get-Text $itemNode "dcsset:use"
$userId = Get-Text $itemNode "dcsset:userSettingID"
# viewMode: detect presence (даже = 'Normal') чтобы compile сделал bit-perfect
$vmNode = $itemNode.SelectSingleNode("dcsset:viewMode", $ns)
$viewMode = if ($vmNode) { $vmNode.InnerText } else { $null }
$userPresNode = $itemNode.SelectSingleNode("dcsset:userSettingPresentation", $ns)
# presentation (multilang or string) на самом filter item
$fiPresNode = $itemNode.SelectSingleNode("dcsset:presentation", $ns)
$fiPres = $null
if ($fiPresNode) {
$fiPres = Get-MLText $fiPresNode
if (-not $fiPres) { $fiPres = $fiPresNode.InnerText }
}
$flags = @()
if ($use -eq 'false') { $flags += '@off' }
if ($userId) { $flags += '@user' }
if ($viewMode -eq 'QuickAccess') { $flags += '@quickAccess' }
elseif ($viewMode -eq 'Inaccessible') { $flags += '@inaccessible' }
# Normal: явное присутствие <viewMode>Normal</viewMode> в XML сохраняется
# через shorthand-флаг @normal (отсутствие — без флага). Это эквивалентно
# object form "viewMode": "Normal" но компактнее.
elseif ($viewMode -eq 'Normal') { $flags += '@normal' }
# nullity ops have no value
$noValueOps = @('filled','notFilled')
# Переход в object form:
# - userSettingPresentation,
# - massivное value (multi-right или пустой ValueList),
# - явный valueType (например, dcscor:Field — field-to-field comparison),
# - presentation на item (multilang или просто текст)
if ($userPresNode -or $valueIsArrayFlag -or $valueTypeAttr -or $fiPres) {
$obj = [ordered]@{ field = $field; op = $op }
if ($op -notin $noValueOps -and $null -ne $value) {
if ($valueIsArrayFlag) {
# Принудительный массив (для empty ValueList тоже)
$arrAsList = New-Object System.Collections.ArrayList
foreach ($vv in @($value)) { [void]$arrAsList.Add($vv) }
$obj['value'] = $arrAsList
} else {
$obj['value'] = $value
}
}
if ($valueTypeAttr) { $obj['valueType'] = $valueTypeAttr }
if ($use -eq 'false') { $obj['use'] = $false }
if ($userId) { $obj['userSettingID'] = 'auto' }
if ($fiPres) { $obj['presentation'] = $fiPres }
if ($viewMode) { $obj['viewMode'] = $viewMode }
if ($userPresNode) { $obj['userSettingPresentation'] = Get-MLText $userPresNode }
return $obj
}
# shorthand
$s = $field
if ($op -in $noValueOps) {
$s += " $op"
} else {
$vDisplay = if ($null -ne $value -and "$value" -ne '') { "$value" } else { '_' }
$s += " $op $vDisplay"
}
if ($flags) { $s += ' ' + ($flags -join ' ') }
return $s
}
# Recursive helper для одного элемента selection. Возвращает либо строку (имя поля / "Auto"),
# либо ordered hashtable ({field, title} / {folder, items: [...]} / sentinel).
function Build-SelectionItem {
param($item, [string]$loc)
$xt = Get-LocalXsiType $item
# Implicit SelectedItemField: <item> без xsi:type, но с <field>
if (-not $xt) {
$fName = Get-Text $item "dcsset:field"
if ($fName) { return $fName }
# Пустой <field/> → wildcard (apply to all) — эквивалентно Auto
$fieldEl = $item.SelectSingleNode("dcsset:field", $ns)
if ($fieldEl) { return 'Auto' }
}
switch ($xt) {
'SelectedItemAuto' {
# Auto может иметь <use>false</use> — отключённый Auto-элемент в selection.
$useV = Get-Text $item "dcsset:use"
if ($useV -eq 'false') {
return [ordered]@{ auto = $true; use = $false }
}
return 'Auto'
}
'SelectedItemField' {
$fName = Get-Text $item "dcsset:field"
$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
$title = Get-MLText $titleNode
$vmN = $item.SelectSingleNode("dcsset:viewMode", $ns)
$useV = Get-Text $item "dcsset:use"
$useFalse = ($useV -eq 'false')
if ($title -or $vmN -or $useFalse) {
$obj = [ordered]@{ field = $fName }
if ($useFalse) { $obj['use'] = $false }
if ($title) { $obj['title'] = $title }
if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
return $obj
}
return $fName
}
'SelectedItemFolder' {
$titleNode = $item.SelectSingleNode("dcsset:lwsTitle", $ns)
$folderTitle = Get-MLText $titleNode
$inner = @()
foreach ($sub in $item.SelectNodes("dcsset:item", $ns)) {
$inner += (Build-SelectionItem -item $sub -loc "$loc/folder")
}
$entry = [ordered]@{ folder = $folderTitle; items = $inner }
# folder может также иметь свой <dcsset:field> (редко, но встречается)
$folderField = Get-Text $item "dcsset:field"
if ($folderField) { $entry['field'] = $folderField }
return $entry
}
default {
return (New-Sentinel -kind "SelectionItem:$xt" -loc $loc -detail 'Неизвестный тип элемента selection')
}
}
}
# Build selection items array
function Build-Selection {
param($selNode, [string]$loc)
if (-not $selNode) { return @() }
$out = @()
foreach ($it in $selNode.SelectNodes("dcsset:item", $ns)) {
$out += (Build-SelectionItem -item $it -loc $loc)
}
return ,$out
}
# Build order items array
function Build-Order {
param($ordNode, [string]$loc)
if (-not $ordNode) { return @() }
$out = @()
foreach ($it in $ordNode.SelectNodes("dcsset:item", $ns)) {
$xt = Get-LocalXsiType $it
switch ($xt) {
'OrderItemAuto' { $out += 'Auto' }
'OrderItemField' {
$fn = Get-Text $it "dcsset:field"
$ot = Get-Text $it "dcsset:orderType"
$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
$useV = Get-Text $it "dcsset:use"
$useFalse = ($useV -eq 'false')
if ($vmN -or $useFalse) {
$obj = [ordered]@{ field = $fn }
if ($useFalse) { $obj['use'] = $false }
if ($ot -eq 'Desc') { $obj['direction'] = 'desc' }
if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
$out += $obj
} else {
if ($ot -eq 'Desc') { $out += "$fn desc" } else { $out += $fn }
}
}
default { $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]@{}
# Silent-drop: scope (fields/groups/overall) — не воспроизводится в DSL
$scopeNode = $it.SelectSingleNode("dcsset:scope", $ns)
if ($scopeNode -and $scopeNode.HasChildNodes) {
$null = Add-Warning -kind 'SilentDrop:scope' -loc "$loc/$i/scope" -detail "conditionalAppearance item имеет scope — не воспроизводится в DSL"
}
$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 }
$presNode = $it.SelectSingleNode("dcsset:presentation", $ns)
if ($presNode) {
$pres = Get-MLText $presNode
if (-not $pres) { $pres = $presNode.InnerText }
if ($pres) { $entry['presentation'] = $pres }
}
$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
if ($vmN) { $entry['viewMode'] = $vmN.InnerText }
$usid = Get-Text $it "dcsset:userSettingID"
if ($usid) { $entry['userSettingID'] = 'auto' }
# use=false на самом condAppearance item
$useV = Get-Text $it "dcsset:use"
if ($useV -eq 'false') { $entry['use'] = $false }
# useInXxx — управляет где применяется правило оформления
# (group, hierarchicalGroup, overall, fieldsHeader, header, parameters,
# filter, resourceFieldsHeader, overallHeader, overallResourceFieldsHeader)
$useInDontUse = @()
foreach ($ch in $it.ChildNodes) {
if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne 'http://v8.1c.ru/8.1/data-composition-system/settings') { continue }
if ($ch.LocalName -match '^useIn(.+)$' -and $ch.InnerText -eq 'DontUse') {
# Преобразуем useInGroup → group, useInFieldsHeader → fieldsHeader
$shortName = ($matches[1]).Substring(0, 1).ToLower() + ($matches[1]).Substring(1)
$useInDontUse += $shortName
}
}
if ($useInDontUse.Count -gt 0) { $entry['useInDontUse'] = $useInDontUse }
$out += $entry
$i++
}
return ,$out
}
# 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') { $rawVal = Get-MLText $val }
else { $rawVal = $val.InnerText }
# <dcscor:use>false</...> → wrapper {value, use: false}
$useV = Get-Text $it "dcscor:use"
if ($useV -eq 'false') {
$d[$pName] = [ordered]@{ value = $rawVal; use = $false }
} else {
$d[$pName] = $rawVal
}
}
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. Простые поля → string. С нестандартным groupType/periodAdditionType
# → object form {field, groupType?, periodAdditionType?} (compile принимает оба варианта).
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 'GroupItemAuto') {
$gFields += 'Auto'
} elseif ($gxt -eq 'GroupItemField') {
$gf = Get-Text $gItem "dcsset:field"
$pat = Get-Text $gItem "dcsset:periodAdditionType"
$gt = Get-Text $gItem "dcsset:groupType"
$isDefault = (-not $pat -or $pat -eq 'None') -and (-not $gt -or $gt -eq 'Items')
if ($isDefault) {
$gFields += $gf
} else {
$obj = [ordered]@{ field = $gf }
if ($gt -and $gt -ne 'Items') { $obj['groupType'] = $gt }
if ($pat -and $pat -ne 'None') { $obj['periodAdditionType'] = $pat }
$gFields += $obj
}
} 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]@{}
# name доступен на любой оси (column/row/point/series): убираем флаг 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 }
# filter block on column/row/point/series
$fNode = $node.SelectSingleNode("dcsset:filter", $ns)
if ($fNode -and $fNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$fa = @()
foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { $fa += (Build-FilterItem -itemNode $fc -loc "$loc/filter") }
$entry['filter'] = $fa
}
# order — preserve presence (even [Auto]) for bit-perfect round-trip
$ordNode = $node.SelectSingleNode("dcsset:order", $ns)
if ($ordNode) {
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
if ($ordItems.Count -gt 0) { $entry['order'] = $ordItems }
}
# selection — preserve presence (even [Auto])
$selNode = $node.SelectSingleNode("dcsset:selection", $ns)
if ($selNode) {
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems }
}
# conditionalAppearance block
$caN = $node.SelectSingleNode("dcsset:conditionalAppearance", $ns)
if ($caN) {
$ca = Build-ConditionalAppearance -caNode $caN -loc "$loc/ca"
if ($ca.Count -gt 0) { $entry['conditionalAppearance'] = $ca }
}
# outputParameters block
$opNode = $node.SelectSingleNode("dcsset:outputParameters", $ns)
$op = Build-OutputParameters -opNode $opNode
if ($op -and $op.Count -gt 0) { $entry['outputParameters'] = $op }
# nested children (StructureItemGroup внутри table row/column или chart axis)
$children = Build-Structure -node $node -loc "$loc/children"
if ($children.Count -gt 0) { $entry['children'] = $children }
# user-settings on the axis itself
# viewMode: сохраняем даже Normal если node присутствует
$avmNode = $node.SelectSingleNode("dcsset:viewMode", $ns)
if ($avmNode) { $entry['viewMode'] = $avmNode.InnerText }
$ausid = Get-Text $node "dcsset:userSettingID"
if ($ausid) { $entry['userSettingID'] = 'auto' }
$ausPresNode = $node.SelectSingleNode("dcsset:userSettingPresentation", $ns)
if ($ausPresNode) {
$ausPres = Get-MLText $ausPresNode
if ($ausPres) { $entry['userSettingPresentation'] = $ausPres }
}
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 }
# top-level selection / outputParameters / conditionalAppearance на самой
# таблице (отдельно от row/column)
$tSelN = $it.SelectSingleNode("dcsset:selection", $ns)
if ($tSelN) {
$tSelI = Build-Selection -selNode $tSelN -loc "$loc/$idx/selection"
if ($tSelI.Count -gt 0) { $entry['selection'] = $tSelI }
}
$tOpN = $it.SelectSingleNode("dcsset:outputParameters", $ns)
$tOp = Build-OutputParameters -opNode $tOpN
if ($tOp -and $tOp.Count -gt 0) { $entry['outputParameters'] = $tOp }
$tCaN = $it.SelectSingleNode("dcsset:conditionalAppearance", $ns)
if ($tCaN) {
$tCa = Build-ConditionalAppearance -caNode $tCaN -loc "$loc/$idx/ca"
if ($tCa.Count -gt 0) { $entry['conditionalAppearance'] = $tCa }
}
$items += $entry
$idx++
continue
}
if ($xt -eq 'StructureItemNestedObject') {
$entry = [ordered]@{ type = 'nestedObject' }
$objID = Get-Text $it "dcsset:objectID"
if ($objID) { $entry['objectID'] = $objID }
$settingsNode = $it.SelectSingleNode("dcsset:settings", $ns)
if ($settingsNode) {
$nestedSettings = [ordered]@{}
$selNode = $settingsNode.SelectSingleNode("dcsset:selection", $ns)
$selI = Build-Selection -selNode $selNode -loc "$loc/$idx/nested/selection"
if ($selI.Count -gt 0) { $nestedSettings['selection'] = $selI }
$fNode = $settingsNode.SelectSingleNode("dcsset:filter", $ns)
if ($fNode -and $fNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$fa = @()
foreach ($fc in $fNode.SelectNodes("dcsset:item", $ns)) { $fa += (Build-FilterItem -itemNode $fc -loc "$loc/$idx/nested/filter") }
$nestedSettings['filter'] = $fa
}
$oNode = $settingsNode.SelectSingleNode("dcsset:order", $ns)
$oI = Build-Order -ordNode $oNode -loc "$loc/$idx/nested/order"
if ($oI.Count -gt 0) { $nestedSettings['order'] = $oI }
$caNode = $settingsNode.SelectSingleNode("dcsset:conditionalAppearance", $ns)
if ($caNode) {
$ca = Build-ConditionalAppearance -caNode $caNode -loc "$loc/$idx/nested/ca"
if ($ca.Count -gt 0) { $nestedSettings['conditionalAppearance'] = $ca }
}
$opNode = $settingsNode.SelectSingleNode("dcsset:outputParameters", $ns)
$op = Build-OutputParameters -opNode $opNode
if ($op -and $op.Count -gt 0) { $nestedSettings['outputParameters'] = $op }
$entry['settings'] = $nestedSettings
}
$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
}
# <dcsset:item> без xsi:type → StructureItemGroup (default form, встречается
# во вложенных children внутри table row / structure group)
if ($xt -and $xt -ne 'StructureItemGroup') {
$items += (New-Sentinel -kind "StructureItem:$xt" -loc $loc -detail 'Тип структуры пока не покрыт')
$idx++
continue
}
$entry = [ordered]@{}
# use=false на самой группе — отключённая ветка структуры
$gUse = Get-Text $it "dcsset:use"
if ($gUse -eq 'false') { $entry['use'] = $false }
# Optional name
$nm = Get-Text $it "dcsset:name"
if ($nm) { $entry['name'] = $nm }
# groupItems → groupFields (через общий Get-GroupFields с object form поддержкой)
$gFields = Get-GroupFields -parentNode $it -loc $loc
if ($gFields.Count -gt 0) { $entry['groupFields'] = $gFields }
# Local selection — preserve presence (even [Auto]) for bit-perfect round-trip
$selNode = $it.SelectSingleNode("dcsset:selection", $ns)
if ($selNode) {
$selItems = Build-Selection -selNode $selNode -loc "$loc/selection"
if ($selItems.Count -gt 0) { $entry['selection'] = $selItems }
}
# Local order — same
$ordNode = $it.SelectSingleNode("dcsset:order", $ns)
if ($ordNode) {
$ordItems = Build-Order -ordNode $ordNode -loc "$loc/order"
if ($ordItems.Count -gt 0) { $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
}
# Local conditionalAppearance
$caNode = $it.SelectSingleNode("dcsset:conditionalAppearance", $ns)
if ($caNode) {
$ca = Build-ConditionalAppearance -caNode $caNode -loc "$loc/ca"
if ($ca.Count -gt 0) { $entry['conditionalAppearance'] = $ca }
}
# Local outputParameters
$opNode = $it.SelectSingleNode("dcsset:outputParameters", $ns)
$op = Build-OutputParameters -opNode $opNode
if ($op -and $op.Count -gt 0) { $entry['outputParameters'] = $op }
# Children — recursive
$children = Build-Structure -node $it -loc "$loc/children"
if ($children.Count -gt 0) { $entry['children'] = $children }
# viewMode / itemsViewMode / userSettingID / userSettingPresentation
# на самой группе. Читаем direct-child <dcsset:*> (избегаем item-level
# vars из selection/filter/order).
$gvm = $null; $givm = $null; $gusid = $null; $guspNode = $null
foreach ($ch in $it.ChildNodes) {
if ($ch.NodeType -ne 'Element' -or $ch.NamespaceURI -ne 'http://v8.1c.ru/8.1/data-composition-system/settings') { continue }
if ($ch.LocalName -eq 'viewMode' -and $null -eq $gvm) { $gvm = $ch.InnerText }
elseif ($ch.LocalName -eq 'itemsViewMode' -and $null -eq $givm) { $givm = $ch.InnerText }
elseif ($ch.LocalName -eq 'userSettingID' -and $null -eq $gusid) { $gusid = $ch.InnerText }
elseif ($ch.LocalName -eq 'userSettingPresentation' -and $null -eq $guspNode) { $guspNode = $ch }
}
# Preserve explicit values (even Normal) so compile bit-perfect roundtrip works:
# platform emits viewMode on some StructureItemGroup shapes but not others.
if ($null -ne $gvm) { $entry['viewMode'] = $gvm }
if ($null -ne $givm) { $entry['itemsViewMode'] = $givm }
if ($gusid) { $entry['userSettingID'] = 'auto' }
if ($guspNode) {
$gusp = Get-MLText $guspNode
if ($gusp) { $entry['userSettingPresentation'] = $gusp }
}
$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 }
if ($cur.Contains('viewMode')) { return $null }
if ($cur.Contains('itemsViewMode')) { return $null }
if ($cur.Contains('userSettingID')) { return $null }
if ($cur.Contains('userSettingPresentation')) { return $null }
if ($cur.Contains('use')) { return $null }
if ($cur.Contains('conditionalAppearance')) { return $null }
if ($cur.Contains('outputParameters')) { 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 }
# Только простые имена-строки сворачиваем в shorthand
if ($gfs[0] -isnot [string]) { 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 ---
# Резолв outputPath и загрузка user-стилей до обработки шаблонов
$script:outputDir = $null
$script:outputBasename = $null
if ($OutputPath) {
if (-not [System.IO.Path]::IsPathRooted($OutputPath)) {
$OutputPath = Join-Path (Get-Location).Path $OutputPath
}
$script:outputDir = [System.IO.Path]::GetDirectoryName($OutputPath)
$script:outputBasename = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath)
Load-UserStyles -dirPath $script:outputDir
}
$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' {
$queryText = Get-Text $dsNode "r:query"
$ds['query'] = Maybe-ExternalizeQuery -queryText $queryText -datasetName $name
}
'DataSetObject' {
$ds['objectName'] = Get-Text $dsNode "r:objectName"
}
'DataSetUnion' {
$nested = @()
$ni = 0
# Inner Union datasets are wrapped as <item xsi:type="..."> in real 1C output.
# Accept <dataSet> too for backward compatibility with older builds.
$innerNodes = @($dsNode.SelectNodes("r:item", $ns)) + @($dsNode.SelectNodes("r:dataSet", $ns))
foreach ($nNode in $innerNodes) {
$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 -eq 'DataSetFieldField') {
$fields += (Build-Field -fieldNode $fn -loc "$loc/field[$fi]")
} elseif ($fxsi -eq 'DataSetFieldFolder') {
# Поле-папка для UI-группировки (только dataPath+title, без типа/роли)
$folderObj = [ordered]@{
field = (Get-Text $fn "r:dataPath")
folder = $true
}
$titleNode = $fn.SelectSingleNode("r:title", $ns)
$title = Get-MLText $titleNode
if ($title) { $folderObj['title'] = $title }
$fields += $folderObj
} else {
$fields += (New-Sentinel -kind "FieldType:$fxsi" -loc "$loc/field[$fi]" -detail 'Тип поля не DataSetFieldField/Folder')
}
$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++
}
# --- 5a-bis. dataSetLinks ---
$dataSetLinks = @()
$dslNodes = $root.SelectNodes("r:dataSetLink", $ns)
foreach ($dslNode in $dslNodes) {
$link = [ordered]@{}
$link['sourceDataSet'] = Get-Text $dslNode.SelectSingleNode("r:sourceDataSet", $ns)
$link['destinationDataSet'] = Get-Text $dslNode.SelectSingleNode("r:destinationDataSet", $ns)
$link['sourceExpression'] = Get-Text $dslNode.SelectSingleNode("r:sourceExpression", $ns)
$link['destinationExpression'] = Get-Text $dslNode.SelectSingleNode("r:destinationExpression", $ns)
$pNode = $dslNode.SelectSingleNode("r:parameter", $ns)
if ($pNode) { $link['parameter'] = Get-Text $pNode }
$plaNode = $dslNode.SelectSingleNode("r:parameterListAllowed", $ns)
if ($plaNode -and ((Get-Text $plaNode) -eq 'true')) { $link['parameterListAllowed'] = $true }
$seNode = $dslNode.SelectSingleNode("r:startExpression", $ns)
if ($seNode) { $link['startExpression'] = Get-Text $seNode }
$lceNode = $dslNode.SelectSingleNode("r:linkConditionExpression", $ns)
if ($lceNode) { $link['linkConditionExpression'] = Get-Text $lceNode }
$dataSetLinks += $link
}
# --- 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 ($dataSetLinks.Count -gt 0) { $out['dataSetLinks'] = $dataSetLinks }
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]@{}
# Helper: read block-level <dcsset:viewMode> (direct child, not item-level)
function Get-BlockVM($node) {
if (-not $node) { return $null }
foreach ($child in $node.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'viewMode' -and $child.NamespaceURI -eq 'http://v8.1c.ru/8.1/data-composition-system/settings') {
return $child.InnerText
}
}
return $null
}
# userFields — пользовательские вычисляемые поля (Expression / Case)
$ufNode = $settingsNode.SelectSingleNode("dcsset:userFields", $ns)
if ($ufNode) {
$ufList = @()
$ufi = 0
foreach ($ufItem in $ufNode.SelectNodes("dcsset:item", $ns)) {
$uxt = Get-LocalXsiType $ufItem
$entry = [ordered]@{}
$dp = Get-Text $ufItem "dcsset:dataPath"
if ($dp) { $entry['dataPath'] = $dp }
$titleN = $ufItem.SelectSingleNode("dcsset:lwsTitle", $ns)
$titleV = Get-MLText $titleN
if ($titleV) { $entry['title'] = $titleV }
if ($uxt -eq 'UserFieldExpression') {
$dEx = Get-Text $ufItem "dcsset:detailExpression"
$dEp = Get-Text $ufItem "dcsset:detailExpressionPresentation"
$tEx = Get-Text $ufItem "dcsset:totalExpression"
$tEp = Get-Text $ufItem "dcsset:totalExpressionPresentation"
if ($dEx -or $dEp) {
$d = [ordered]@{}
if ($dEx) { $d['expression'] = $dEx }
if ($dEp) { $d['presentation'] = $dEp }
$entry['detail'] = $d
}
if ($tEx -or $tEp) {
$t = [ordered]@{}
if ($tEx) { $t['expression'] = $tEx }
if ($tEp) { $t['presentation'] = $tEp }
$entry['total'] = $t
}
} elseif ($uxt -eq 'UserFieldCase') {
$casesNode = $ufItem.SelectSingleNode("dcsset:cases", $ns)
$casesArr = @()
if ($casesNode) {
foreach ($caseItem in $casesNode.SelectNodes("dcsset:item", $ns)) {
$ce = [ordered]@{}
$cfNode = $caseItem.SelectSingleNode("dcsset:filter", $ns)
if ($cfNode -and $cfNode.SelectNodes("dcsset:item", $ns).Count -gt 0) {
$cfa = @()
foreach ($cfi in $cfNode.SelectNodes("dcsset:item", $ns)) { $cfa += (Build-FilterItem -itemNode $cfi -loc "variant[$vi]/userField/case/filter") }
$ce['filter'] = $cfa
}
$cvNode = $caseItem.SelectSingleNode("dcsset:value", $ns)
if ($cvNode) {
$cvType = Get-LocalXsiType $cvNode
$cvText = $cvNode.InnerText
if ($cvType -eq 'boolean') { $ce['value'] = ($cvText -eq 'true') }
elseif ($cvType -eq 'decimal') {
if ($cvText -match '^-?\d+$') { $ce['value'] = [int]$cvText }
else { $ce['value'] = [double]$cvText }
}
else { $ce['value'] = $cvText }
}
$cpNode = $caseItem.SelectSingleNode("dcsset:lwsPresentationValue", $ns)
$cpV = Get-MLText $cpNode
if ($cpV) { $ce['presentation'] = $cpV }
$casesArr += $ce
}
}
$entry['cases'] = $casesArr
} else {
$entry['__unsupported__'] = (New-Sentinel -kind "UserField:$uxt" -loc "variant[$vi]/userField[$ufi]" -detail 'Неизвестный тип пользовательского поля')['__unsupported__']
}
$ufList += $entry
$ufi++
}
if ($ufList.Count -gt 0) { $settings['userFields'] = $ufList }
}
# 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 }
# Block-level viewMode: preserve exact presence (even Normal) for bit-perfect round-trip
$svm = Get-BlockVM $selTop
if ($null -ne $svm) { $settings['selectionViewMode'] = $svm }
# 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
}
$fvm = Get-BlockVM $fTop
if ($null -ne $fvm) { $settings['filterViewMode'] = $fvm }
# order
$ordTop = $settingsNode.SelectSingleNode("dcsset:order", $ns)
$ordItems = Build-Order -ordNode $ordTop -loc "variant[$vi]/order"
if ($ordItems.Count -gt 0) { $settings['order'] = $ordItems }
$ovm = Get-BlockVM $ordTop
if ($null -ne $ovm) { $settings['orderViewMode'] = $ovm }
# 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 }
}
$cavm = Get-BlockVM $caTop
if ($null -ne $cavm) { $settings['conditionalAppearanceViewMode'] = $cavm }
# 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 }
}
# <dcsset:itemsViewMode> on settings — preserve presence (even Normal)
$sivmNode = $settingsNode.SelectSingleNode("dcsset:itemsViewMode", $ns)
if ($sivmNode) { $settings['itemsViewMode'] = $sivmNode.InnerText }
# <dcsset:additionalProperties> — key→value свойства варианта (URL, имя, GUID и т.п.)
$apNode = $settingsNode.SelectSingleNode("dcsset:additionalProperties", $ns)
if ($apNode) {
$apDict = [ordered]@{}
foreach ($prop in $apNode.SelectNodes("v8:Property", $ns)) {
$pName = $prop.GetAttribute("name")
$valEl = $prop.SelectSingleNode("v8:Value", $ns)
if ($pName -and $valEl) { $apDict[$pName] = $valEl.InnerText }
}
if ($apDict.Count -gt 0) { $settings['additionalProperties'] = $apDict }
}
# 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 = ConvertTo-CompactJson -obj $out
if ($OutputPath) {
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($OutputPath, $json, $enc)
Save-UserStyles -dirPath $script:outputDir
Save-QueryFiles
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)")
}
}
}