Files
cc-1c-skills/.claude/skills/skd-decompile/scripts/skd-decompile.ps1
T
Nick Shirokov e03ba3b509 fix(skd-decompile): сворачивать дефолтные auto selection/order группы в shorthand
После 6781bb3 компилятор кладёт в каждую группу структуры авто-поле выбора и
авто-порядок (SelectedItemAuto/OrderItemAuto), как делает платформа. decompile
сохранял их как selection:["Auto"]/order:["Auto"], из-за чего Try-StructureShorthand
не сворачивал цепочку и выдавал громоздкую объектную модель вместо строки
"A > B > details".

Теперь selection/order, состоящие ровно из одного "Auto" (предикат Is-AutoOnly /
is_auto_only), считаются дефолтом и не мешают свёртке. Round-trip снова bit-perfect:
shorthand перекомпилируется в идентичный XML (Parse-StructureShorthand сам добавляет
эти auto-поля). Отключённый auto ({auto,use}), смешанные списки и явные поля свёртку
не проходят и остаются в объектной форме.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:12:50 +03:00

3037 lines
128 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.90 — 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"
}
# Пустые dataSets — отчёт без источника данных (только settingsVariant с outputParameters).
# Такие отчёты валидны (динамическое заполнение из кода), но compile требует ≥1 dataSet,
# и весь DSL заточен под data-driven отчёты — fail-fast.
if ($xmlDoc.SelectNodes("//*[local-name()='dataSet']").Count -eq 0) {
Fail-Ring3 -kind "отчёт без dataSet (служебный шаблон-обёртка)" -loc "DataCompositionSchema/dataSet"
}
# --- 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 }
if ([string]::IsNullOrEmpty($xpath)) { return $node.InnerText }
$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)
$typeSets = $valueTypeNode.SelectNodes("v8:TypeSet", $ns)
if ($types.Count -eq 0 -and $typeSets.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) }
# TypeSet (композитный тип-набор) — извлекаем local-name из значения "<prefix>:Name".
foreach ($ts in $typeSets) {
$txt = $ts.InnerText
if ($txt -match ':(.+)$') { $shorts += $matches[1] }
else { $shorts += $txt }
}
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-FontValue {
param($valNode)
# Шрифт: <dcscor:value xsi:type="v8ui:Font" ref=... faceName=... height=... bold=... .../>
# Сохраняем все атрибуты как объект {@type:Font, ...}, чтобы compile мог восстановить bit-perfect.
$f = [ordered]@{ '@type' = 'Font' }
foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
$a = $valNode.Attributes[$attrName]
if ($null -ne $a) { $f[$attrName] = $a.Value }
}
return $f
}
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, v8ui:Font, etc.
$valType = Get-LocalXsiType $valNode
if ($valType -eq 'LocalStringType') {
$rawVal = Get-MLText $valNode
} elseif ($valType -eq 'Font') {
$rawVal = Get-FontValue $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)) {
$vXsi = Get-LocalXsiType $v
$vTxt = $v.InnerText
if ($vXsi -eq 'boolean') {
$values += ($vTxt -eq 'true')
} elseif ($vXsi -eq 'decimal') {
if ($vTxt -match '^-?\d+$') { $values += [int]$vTxt }
else { $values += [double]$vTxt }
} else {
$values += $vTxt
}
}
$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
# Сохраняем кастомный xsi:type (например, "d6p1:FoldersAndItemsUse" с локальным xmlns).
# Не сохраняем xs:* (string/dateTime/etc) — compile auto-detect.
$ta = $val.Attributes['xsi:type']
if ($ta -and $ta.Value -notmatch '^xs:') {
$prefix = ($ta.Value -split ':', 2)[0]
$localName = ($ta.Value -split ':', 2)[1]
$uri = $val.GetNamespaceOfPrefix($prefix)
if ($uri) {
$entry['valueType'] = [ordered]@{ uri = $uri; name = $localName }
}
}
}
}
}
$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.
# На одном поле может быть несколько <orderExpression> (multi-sort fallback),
# в этом случае сохраняем массив; единичный — как объект (back-compat).
$orderExprNodes = $fieldNode.SelectNodes("r:orderExpression", $ns)
$orderExpression = $null
$orderExpressionList = @()
foreach ($oeN in $orderExprNodes) {
$oeExpr = Get-Text $oeN "dcscom:expression"
$oeType = Get-Text $oeN "dcscom:orderType"
$oeAuto = Get-Text $oeN "dcscom:autoOrder"
$oe = [ordered]@{}
if ($oeExpr) { $oe['expression'] = $oeExpr }
if ($oeType) { $oe['orderType'] = $oeType }
if ($oeAuto -eq 'true') { $oe['autoOrder'] = $true }
elseif ($oeAuto -eq 'false') { $oe['autoOrder'] = $false }
$orderExpressionList += ,$oe
}
if ($orderExpressionList.Count -eq 1) {
$orderExpression = $orderExpressionList[0]
} elseif ($orderExpressionList.Count -gt 1) {
$orderExpression = $orderExpressionList
}
$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 — может быть несколько (valueListAllowed: список значений по умолчанию).
$valueNodes = $pNode.SelectNodes("r:value", $ns)
$valueDisplay = $null
$valueIsNil = $false
if ($valueNodes.Count -gt 1) {
# Multi-value (список значений по умолчанию для параметра-списка)
$valueArr = @()
foreach ($vn in $valueNodes) {
$vt = Get-LocalXsiType $vn
$vTxt = $vn.InnerText
if ($vt -eq 'boolean') { $valueArr += ($vTxt -eq 'true') }
elseif ($vt -eq 'decimal') {
if ($vTxt -match '^-?\d+$') { $valueArr += [int]$vTxt }
else { $valueArr += [double]$vTxt }
} else { $valueArr += $vTxt }
}
$valueDisplay = $valueArr
}
elseif ($valueNodes.Count -eq 1) {
$valueNode = $valueNodes[0]
$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"
$sd = Get-Text $valueNode "v8:startDate"
$ed = Get-Text $valueNode "v8:endDate"
$hasExplicitDates = ($sd -and $sd -ne '0001-01-01T00:00:00') -or ($ed -and $ed -ne '0001-01-01T00:00:00')
if ($hasExplicitDates) {
# Custom с явными датами → object form {variant, startDate, endDate}
$valueDisplay = [ordered]@{ variant = $variant }
if ($sd) { $valueDisplay['startDate'] = $sd }
if ($ed) { $valueDisplay['endDate'] = $ed }
} elseif ($variant -and $variant -ne 'Custom') {
$valueDisplay = $variant
}
# Custom без явных дат — valueDisplay = null, compile подставит 0001-01-01.
} 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) {
$avType = Get-LocalXsiType $avValNode
$avText = $avValNode.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 ($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=true non-hidden non-autoDates требует object form — иначе shorthand
# теряет этот атрибут (compile эмитит default useRestriction=false).
if ($p.useRestriction -and -not $p.hidden -and -not $p.autoDates) { $needsObject = $true }
if ($p.expression) { $needsObject = $true }
if ($p.notAField) { $needsObject = $true }
# valueIsNil на non-композитном типе требует object form, чтобы compile
# знал что вместо xs:string/xs:decimal-default нужно эмитить xsi:nil="true".
# Для ref-типов compile в любом случае эмитит nil, шорткод покрывает.
$refTypePattern = '^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan|CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|InformationRegisterRef|ExchangePlanRef|AnyRef)'
$typeIsRef = $false
if ($typeShort -is [string] -and $typeShort -match $refTypePattern) { $typeIsRef = $true }
$nilNeedsObject = $valueIsNil -and -not $typeIsRef -and $typeShort -and -not ($typeShort -is [array])
if ($nilNeedsObject) { $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 ($nilNeedsObject) { $obj['nilValue'] = $true }
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 }
# useRestriction явно эмитится только если: true И НЕ покрыт hidden/autoDates (compile auto-emit).
if ($p.useRestriction -and -not $p.hidden -and -not $p.autoDates) { $obj['useRestriction'] = $true }
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
# Префикс xsi:type или value — резолвим в URI и выбираем DSL-префикс.
if ($txt -match '^([^:]+):(.+)$') {
$pfx = $matches[1]
$name = $matches[2]
$uri = $valNode.GetNamespaceOfPrefix($pfx)
switch ($uri) {
'http://v8.1c.ru/8.1/data/ui/style' { return 'style:' + $name }
'http://v8.1c.ru/8.1/data/ui/colors/web' { return 'web:' + $name }
'http://v8.1c.ru/8.1/data/ui/colors/windows' { return 'win:' + $name }
}
}
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)
# drilldown — суффикс X из имени Расшифровка_X (только shortcut form B).
# drilldownTarget — полное имя target-параметра как есть (любая форма).
$attrs = @{ width = $null; height = $null; mergeV = $false; mergeH = $false; drilldown = $null; drilldownTarget = $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> или Расшифровка_<X>
if ($val) {
$paramRef = $val.InnerText
$attrs.drilldownTarget = $paramRef
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)
$cellDrilldownMap = @{} # "r,c" → полное имя drilldown-target (для cell wrap в object-form)
$hasAnyStyledCell = $false
$drilldownByParam = @{} # param name → field name (X from Расшифровка_X) — для form B fold
$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 — для shortcut form B (Расшифровка_X) кладём в drilldownByParam.
# Полное имя target сохраняем в cellDrilldownMap для последующего разрешения:
# если target = "Расшифровка_X" и X совпадает с именем параметра ячейки {X} —
# это shortcut и cell остаётся строкой; иначе cell wrap в {value, drilldown}.
if ($content -match '^\{(.+)\}$' -and $perCell.drilldown) {
$drilldownByParam[$matches[1]] = $perCell.drilldown
}
if ($perCell.drilldownTarget) {
$cellDrilldownMap["$rowIdx,$colIdx"] = $perCell.drilldownTarget
}
# 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]@{}
$detailsByName = [ordered]@{} # name → @{ field, expression, action }
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') {
$feNode = $pn.SelectSingleNode("dcsat:fieldExpression", $ns)
$detailsByName[$pName] = @{
field = if ($feNode) { Get-Text $feNode "dcsat:field" } else { '' }
expression = if ($feNode) { Get-Text $feNode "dcsat:expression" } else { '' }
action = Get-Text $pn "dcsat:mainAction"
}
}
}
# Сворачиваем shortcut form B: каждый exprParam X с drilldownByParam[X]=Y проверяем —
# если detailsByName["Расшифровка_Y"] существует и имеет canonical shape
# (field=ИмяРесурса, expression="Y", action=DrillDown) → fold X.drilldown="Y" и mark detail folded.
$foldedDetailNames = @{}
foreach ($pname in @($drilldownByParam.Keys)) {
$yVal = $drilldownByParam[$pname]
$detailName = "Расшифровка_$yVal"
if ($detailsByName.Contains($detailName)) {
$d = $detailsByName[$detailName]
$expectedExpr = "`"$yVal`""
if ($d.field -eq 'ИмяРесурса' -and $d.expression -eq $expectedExpr -and $d.action -eq 'DrillDown') {
$foldedDetailNames[$detailName] = $true
}
}
}
$templateParams = @()
foreach ($pname in $exprParams.Keys) {
$entry = [ordered]@{ name = $pname; expression = $exprParams[$pname] }
if ($drilldownByParam.ContainsKey($pname)) {
$entry['drilldown'] = $drilldownByParam[$pname]
}
$templateParams += $entry
}
# Form C: details-параметры, не свёрнутые как shortcut → отдельная запись.
foreach ($dname in $detailsByName.Keys) {
if ($foldedDetailNames.ContainsKey($dname)) { continue }
$d = $detailsByName[$dname]
$entry = [ordered]@{ name = $dname }
$ddObj = [ordered]@{ field = $d.field; expression = $d.expression }
if ($d.action -and $d.action -ne 'DrillDown') { $ddObj['action'] = $d.action }
$entry['drilldown'] = $ddObj
$templateParams += $entry
}
# Cell wrapping: для ячеек, у которых drilldownTarget НЕ соответствует shortcut form B
# (то есть target ≠ "Расшифровка_X" с X = имя параметра ячейки), оборачиваем в {value, drilldown}.
# Уже обёрнутые style-ом ячейки получают drilldown как дополнительное поле.
if ($cellDrilldownMap.Count -gt 0) {
for ($r = 0; $r -lt $rows.Count; $r++) {
$newRow = @()
for ($c = 0; $c -lt $rows[$r].Count; $c++) {
$cellVal = $rows[$r][$c]
$key = "$r,$c"
$target = $cellDrilldownMap[$key]
# Распаковка inner value если cell уже обёрнута style-ом
$innerVal = $cellVal
$isWrapped = $false
if ($cellVal -is [System.Collections.IDictionary] -or $cellVal -is [hashtable]) {
if ($cellVal.Contains('value')) {
$innerVal = $cellVal['value']
$isWrapped = $true
}
}
$needsWrap = $false
if ($target -and ($innerVal -is [string]) -and ($innerVal -match '^\{(.+)\}$')) {
$cellParam = $matches[1]
# Shortcut form B: cell param имеет drilldown "Y", target == "Расшифровка_Y".
$expectedShortcut = $null
if ($drilldownByParam.ContainsKey($cellParam)) {
$expectedShortcut = "Расшифровка_$($drilldownByParam[$cellParam])"
}
if ($target -ne $expectedShortcut) { $needsWrap = $true }
}
if ($needsWrap) {
if ($isWrapped) {
$cellVal['drilldown'] = $target
$newRow += $cellVal
} else {
$newRow += [ordered]@{ value = $innerVal; drilldown = $target }
}
} else {
$newRow += $cellVal
}
}
$rows[$r] = $newRow
}
}
# 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 } }
# Конвертация по типу — compile различает [bool]/[int]/[double] для auto-detect xsi:type.
if ($vType -eq 'boolean') { return @{ value = ($txt -eq 'true'); type = $rawType } }
if ($vType -eq 'decimal') {
if ($txt -match '^-?\d+$') { return @{ value = [int]$txt; type = $rawType } }
return @{ value = [double]$txt; 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:*).
# DesignTimeValue для значений вида "Перечисление.X.Y" / "Справочник.X.Y" /
# "ПланСчетов.X.Y" и т.п. также auto-detect-ится — не сохраняем.
$autoDetectsDTV = ($vt.type -eq 'dcscor:DesignTimeValue') -and `
("$($vt.value)" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.')
if ($vt.type -and $vt.type -notmatch '^xs:' -and -not $autoDetectsDTV) {
$valueTypeAttr = $vt.type
}
}
} elseif ($rightNodes.Count -gt 1) {
# Несколько значений → массив (InList с конкретными значениями)
$arr = @()
$rawTypes = @()
foreach ($rn in $rightNodes) {
$arr += (Get-FilterValue $rn)
$rawTypes += $rn.GetAttribute("type", $NS_XSI)
}
$value = $arr
$valueIsArrayFlag = $true
# Сохраняем raw xsi:type если все одинаковые — compile будет использовать
# как явный valueType (иначе авто-detect выберет DesignTimeValue для строк
# "Перечисление.*", но оригинал может хранить как xs:string).
# DesignTimeValue для значений-ref-литералов сам авто-detect-ится — не сохраняем.
$uniqTypes = @($rawTypes | Sort-Object -Unique)
if ($uniqTypes.Count -eq 1 -and $uniqTypes[0]) {
$autoDetectsDTV = ($uniqTypes[0] -eq 'dcscor:DesignTimeValue') -and `
($arr.Count -gt 0) -and `
(@($arr | Where-Object { "$_" -notmatch '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.' }).Count -eq 0)
if (-not $autoDetectsDTV) {
$valueTypeAttr = $uniqTypes[0]
}
}
}
$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) {
if ($value -is [bool]) { $vDisplay = if ($value) { 'true' } else { 'false' } }
elseif ("$value" -ne '') { $vDisplay = "$value" }
}
$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 }
$plN = $item.SelectSingleNode("dcsset:placement", $ns)
if ($plN -and $plN.InnerText -and $plN.InnerText -ne 'Auto') {
$entry['placement'] = $plN.InnerText
}
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
}
# Прочитать <dcscor:value xsi:type="v8ui:Line"> в объект {@type:Line, width, gap, style}.
function Get-LineValue {
param($valNode)
$obj = [ordered]@{ '@type' = 'Line' }
$w = $valNode.GetAttribute("width")
$g = $valNode.GetAttribute("gap")
if ($w -ne '') { $obj['width'] = if ($w -match '^-?\d+$') { [int]$w } else { $w } }
if ($g -ne '') { $obj['gap'] = ($g -eq 'true') }
$styleNode = $valNode.SelectSingleNode("v8ui:style", $ns)
if ($styleNode) { $obj['style'] = $styleNode.InnerText }
return $obj
}
# Прочитать <dcscor:value> в JSON-значение: Font/Line/multilang/raw text.
# Возвращает то значение которое идёт в "value" slot.
function Read-AppearanceValueNode {
param($valNode)
if (-not $valNode) { return $null }
$vt = Get-LocalXsiType $valNode
if ($vt -eq 'LocalStringType') { return (Get-MLText $valNode) }
if ($vt -eq 'Font') { return (Get-FontValue $valNode) }
if ($vt -eq 'Line') { return (Get-LineValue $valNode) }
return $valNode.InnerText
}
# Build appearance dict from <dcsset:appearance> or <dcscor:item> list.
# Поддерживает Line-значения (граница) и nested SettingsParameterValue items
# (например СтильГраницы.Сверху). DSL form B (см. docs/skd-dsl-spec.md):
# - top-level Line: { "@type": "Line", "width", "gap", "style", "use"?, "items"? }
# - nested item: { "value": <значение>, "use"?: false }
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 }
$rawVal = Read-AppearanceValueNode $val
$useV = Get-Text $it "dcscor:use"
# Nested dcscor:item внутри этого item — wrap form {value, use?}.
$nestedItems = [ordered]@{}
foreach ($sub in $it.SelectNodes("dcscor:item", $ns)) {
$subName = Get-Text $sub "dcscor:parameter"
$subVal = $sub.SelectSingleNode("dcscor:value", $ns)
if (-not $subName) { continue }
$subRaw = Read-AppearanceValueNode $subVal
$subUse = Get-Text $sub "dcscor:use"
$subEntry = [ordered]@{ value = $subRaw }
if ($subUse -eq 'false') { $subEntry['use'] = $false }
$nestedItems[$subName] = $subEntry
}
# Определяем форму вывода
$valIsLine = ($rawVal -is [System.Collections.IDictionary]) -and $rawVal.Contains('@type') -and ($rawVal['@type'] -eq 'Line')
if ($valIsLine) {
# top-level Line — атрибуты inline + опц. use/items
if ($useV -eq 'false') { $rawVal['use'] = $false }
if ($nestedItems.Count -gt 0) { $rawVal['items'] = $nestedItems }
$dict[$pName] = $rawVal
} elseif (($useV -eq 'false') -or ($nestedItems.Count -gt 0)) {
$wrap = [ordered]@{ value = $rawVal }
if ($useV -eq 'false') { $wrap['use'] = $false }
if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems }
$dict[$pName] = $wrap
} else {
$dict[$pName] = $rawVal
}
}
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' }
$uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
if ($uspN) {
$usp = Get-MLText $uspN
if ($usp) { $entry['userSettingPresentation'] = $usp }
}
# 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
# Зеркало $script:outputParamTypes из skd-compile — для known keys compile auto-detect-ит
# тип по имени параметра, поэтому valueType в DSL не нужен (избыточный шум).
$script:outputParamTypesKnown = @{
'Заголовок' = 'mltext'
'ВыводитьЗаголовок' = 'dcsset:DataCompositionTextOutputType'
'ВыводитьПараметрыДанных' = 'dcsset:DataCompositionTextOutputType'
'ВыводитьОтбор' = 'dcsset:DataCompositionTextOutputType'
'МакетОформления' = 'xs:string'
'РасположениеПолейГруппировки' = 'dcsset:DataCompositionGroupFieldsPlacement'
'РасположениеРеквизитов' = 'dcsset:DataCompositionAttributesPlacement'
'ГоризонтальноеРасположениеОбщихИтогов' = 'dcscor:DataCompositionTotalPlacement'
'ВертикальноеРасположениеОбщихИтогов' = 'dcscor:DataCompositionTotalPlacement'
'РасположениеОбщихИтогов' = 'dcscor:DataCompositionTotalPlacement'
'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement'
'РасположениеГруппировки' = 'dcsset:DataCompositionFieldGroupPlacement'
'РасположениеРесурсов' = 'dcsset:DataCompositionResourcesPlacement'
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
}
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
# Полный xsi:type (с префиксом) — нужен compile для bit-perfect, если ключ не в outputParamTypes.
$fullType = $null
$ta = $val.Attributes['xsi:type']
if ($ta) { $fullType = $ta.Value }
if ($vType -eq 'LocalStringType') { $rawVal = Get-MLText $val }
elseif ($vType -eq 'Font') { $rawVal = Get-FontValue $val }
else { $rawVal = $val.InnerText }
# Nested dcscor:items (sub-параметры типа ТипДиаграммы.ВидПодписей)
$nestedItems = [ordered]@{}
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
$subFull = $null
$subTA = $subVal.Attributes['xsi:type']
if ($subTA) { $subFull = $subTA.Value }
if ($subType -eq 'LocalStringType') { $subRaw = Get-MLText $subVal }
elseif ($subType -eq 'Font') { $subRaw = Get-FontValue $subVal }
else { $subRaw = $subVal.InnerText }
# Резолвим prefix → URI: если URI не из стандартных корневых xmlns —
# сохраняем как объект {uri, name} чтобы compile эмитил xmlns локально.
$subTypeField = $subFull
if ($subFull -and $subFull -match '^([^:]+):(.+)$') {
$pfx = $matches[1]; $localName = $matches[2]
$uri = $subVal.GetNamespaceOfPrefix($pfx)
if ($uri -and $uri -notin @(
'http://www.w3.org/2001/XMLSchema',
'http://www.w3.org/2001/XMLSchema-instance',
'http://v8.1c.ru/8.1/data-composition-system/schema',
'http://v8.1c.ru/8.1/data-composition-system/settings',
'http://v8.1c.ru/8.1/data-composition-system/core',
'http://v8.1c.ru/8.1/data-composition-system/common',
'http://v8.1c.ru/8.1/data/core',
'http://v8.1c.ru/8.1/data/ui'
)) {
$subTypeField = [ordered]@{ uri = $uri; name = $localName }
}
}
$entry = [ordered]@{ value = $subRaw; valueType = $subTypeField }
$subUse = Get-Text $sub "dcscor:use"
if ($subUse -eq 'false') { $entry['use'] = $false }
$nestedItems[$subName] = $entry
}
# Extras (use=false / viewMode / userSettingID / userSettingPresentation / nested items) → wrapper.
$useV = Get-Text $it "dcscor:use"
$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
$usidV = Get-Text $it "dcsset:userSettingID"
$uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
# Если xsi:type — кастомный (dcsset:XXX, v8ui:XXX, и т.п., не xs:* и не LocalStringType/Font),
# нужен wrap чтобы compile сохранил тип через valueType (default — xs:string).
# typeIsCustom: тип не покрыт auto-detect (compile сам сделает default).
# Если ключ — known (есть в outputParamTypesKnown) И значение xsi:type совпадает с map —
# auto-detect compile вернёт тот же тип → valueType в DSL не нужен.
$typeAutoDetected = $false
if ($script:outputParamTypesKnown.ContainsKey($pName)) {
$mapType = $script:outputParamTypesKnown[$pName]
# mltext в map ≡ LocalStringType в XML
if ($mapType -eq 'mltext' -and $vType -eq 'LocalStringType') { $typeAutoDetected = $true }
elseif ($fullType -eq $mapType) { $typeAutoDetected = $true }
}
$typeIsCustom = $fullType -and ($fullType -notmatch '^xs:') -and ($vType -ne 'LocalStringType') -and ($vType -ne 'Font') -and -not $typeAutoDetected
$hasExtras = ($useV -eq 'false') -or $vmN -or $usidV -or $uspN -or ($nestedItems.Count -gt 0) -or $typeIsCustom
if ($hasExtras) {
$wrap = [ordered]@{ value = $rawVal }
if ($fullType -and -not (($vType -eq 'LocalStringType') -or ($vType -eq 'Font')) -and -not $typeAutoDetected) {
$wrap['valueType'] = $fullType
}
if ($useV -eq 'false') { $wrap['use'] = $false }
if ($nestedItems.Count -gt 0) { $wrap['items'] = $nestedItems }
if ($vmN) { $wrap['viewMode'] = $vmN.InnerText }
if ($usidV) { $wrap['userSettingID'] = 'auto' }
if ($uspN) { $wrap['userSettingPresentation'] = Get-MLText $uspN }
$d[$pName] = $wrap
} 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 на dataParameter item — это <dcscor:use> (не dcsset)
$use = Get-Text $it "dcscor:use"
if ($use -eq 'false') { $canAuto = $false }
# viewMode / userSettingPresentation на dataParameter item — это dcsset:* (после value)
$vmN = $it.SelectSingleNode("dcsset:viewMode", $ns)
$uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns)
if ($vmN -or $uspN) { $canAuto = $false }
$tp = $visibleTop[$pn]
$flags = @()
if ($usid) { $flags += '@user' }
if ($use -eq 'false') { $flags += '@off' }
$vt = Get-LocalXsiType $valNode
$vDisplay = $null
$stdPeriodObj = $null
if ($vt -eq 'StandardPeriod') {
# Shape inference в compile: {variant, startDate, endDate} → SP с датами,
# {variant} only с SP-вариантом (ThisMonth/Custom/etc) → SP без дат.
$variant = Get-Text $valNode "v8:variant"
$sd = Get-Text $valNode "v8:startDate"
$ed = Get-Text $valNode "v8:endDate"
$hasExplicitDates = ($sd -and $sd -ne '0001-01-01T00:00:00') -or ($ed -and $ed -ne '0001-01-01T00:00:00')
if ($hasExplicitDates) {
$stdPeriodObj = [ordered]@{ variant = $variant }
if ($sd) { $stdPeriodObj['startDate'] = $sd }
if ($ed) { $stdPeriodObj['endDate'] = $ed }
$canAuto = $false
} elseif ($variant) {
$vDisplay = $variant
if ($vmN -or $uspN) {
$stdPeriodObj = [ordered]@{ variant = $variant }
}
}
} elseif ($vt -eq 'StandardBeginningDate') {
# Shape inference в compile: {variant, date} → SBD, либо variant начинается с BeginningOf*.
$variant = Get-Text $valNode "v8:variant"
$d = Get-Text $valNode "v8:date"
$hasExplicitDate = $d -and $d -ne '0001-01-01T00:00:00'
if ($hasExplicitDate) {
$stdPeriodObj = [ordered]@{ variant = $variant; date = $d }
$canAuto = $false
} elseif ($variant) {
$stdPeriodObj = [ordered]@{ variant = $variant }
$canAuto = $false
}
} 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
# Empty xs:string + use=false — оригинальный placeholder для disabled-параметра
# (используется для типа DateTime в settings; см. АнализПлановыхНачислений @1506).
# Сохраняем явный valueType чтобы compile эмитил <value xsi:type="xs:string"/>
# вместо xsi:nil.
$isEmptyStringPlaceholder = ($vt -eq 'string') -and (-not $valNode.InnerText) -and ($use -eq 'false')
if ($isEmptyStringPlaceholder) { $canAuto = $false }
# Object form требуется если есть viewMode / userSettingPresentation / StandardPeriod-с-датами / xs:string-placeholder
if ($stdPeriodObj -or $vmN -or $uspN -or $isEmptyStringPlaceholder) {
$obj = [ordered]@{ parameter = $pn }
if ($stdPeriodObj) {
$obj['value'] = $stdPeriodObj
# valueType не нужен — compile определит StandardPeriod по value.variant
} elseif ($isEmptyStringPlaceholder) {
$obj['value'] = ''
$obj['valueType'] = 'xs:string'
} elseif ($null -ne $vDisplay -and $vDisplay -ne '') {
# Конвертация для типизированных значений (compile различает по типу JSON)
if ($vt -eq 'boolean') { $obj['value'] = ($vDisplay -eq 'true') }
elseif ($vt -eq 'decimal') {
if ($vDisplay -match '^-?\d+$') { $obj['value'] = [int]$vDisplay }
else { $obj['value'] = [double]$vDisplay }
}
else { $obj['value'] = $vDisplay }
# Сохраняем полный xsi:type для bit-perfect эмиссии
$ta = $valNode.Attributes['xsi:type']
if ($ta) { $obj['valueType'] = $ta.Value }
}
if ($use -eq 'false') { $obj['use'] = $false }
if ($usid) { $obj['userSettingID'] = 'auto' }
if ($vmN) { $obj['viewMode'] = $vmN.InnerText }
if ($uspN) {
$uspV = Get-MLText $uspN
if ($uspV) { $obj['userSettingPresentation'] = $uspV }
}
$entries += $obj
} else {
# 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"
# periodAdditionBegin/End: non-default = либо dcscor:Field (path), либо
# date ≠ 0001-01-01T00:00:00. Сохраняем строкой — compile auto-detect тип.
$pabN = $gItem.SelectSingleNode("dcsset:periodAdditionBegin", $ns)
$paeN = $gItem.SelectSingleNode("dcsset:periodAdditionEnd", $ns)
$pab = $null; $pae = $null
if ($pabN) {
$pt = Get-LocalXsiType $pabN
$pv = $pabN.InnerText
if ($pt -eq 'Field' -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pab = $pv }
}
if ($paeN) {
$pt = Get-LocalXsiType $paeN
$pv = $paeN.InnerText
if ($pt -eq 'Field' -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pae = $pv }
}
$isDefault = (-not $pat -or $pat -eq 'None') -and (-not $gt -or $gt -eq 'Items') -and (-not $pab) -and (-not $pae)
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 }
if ($pab) { $obj['periodAdditionBegin'] = $pab }
if ($pae) { $obj['periodAdditionEnd'] = $pae }
$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 }
}
# itemsViewMode на axis (column/row/point/series)
$aivmNode = $node.SelectSingleNode("dcsset:itemsViewMode", $ns)
if ($aivmNode) { $entry['itemsViewMode'] = $aivmNode.InnerText }
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 }
}
# use=false на самой таблице — отключённая ветка
$tUse = Get-Text $it "dcsset:use"
if ($tUse -eq 'false') { $entry['use'] = $false }
# viewMode / userSettingID / userSettingPresentation / itemsViewMode / rowsViewMode / columnsViewMode на самой таблице
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 -not $entry.Contains('viewMode')) { $entry['viewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'userSettingID' -and -not $entry.Contains('userSettingID')) { $entry['userSettingID'] = 'auto' }
elseif ($ch.LocalName -eq 'userSettingPresentation' -and -not $entry.Contains('userSettingPresentation')) {
$uspV = Get-MLText $ch
if ($uspV) { $entry['userSettingPresentation'] = $uspV }
}
elseif ($ch.LocalName -eq 'itemsViewMode' -and -not $entry.Contains('itemsViewMode')) { $entry['itemsViewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'columnsViewMode' -and -not $entry.Contains('columnsViewMode')) { $entry['columnsViewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'rowsViewMode' -and -not $entry.Contains('rowsViewMode')) { $entry['rowsViewMode'] = $ch.InnerText }
}
$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 }
# point/series — может быть несколько (multi-series диаграмма).
# Single → сохраняем как object (backward-compat); >1 → массив.
$pnList = $it.SelectNodes("dcsset:point", $ns)
if ($pnList.Count -eq 1) {
$entry['points'] = Build-TableAxisBlock -node $pnList[0] -loc "$loc/$idx/point"
} elseif ($pnList.Count -gt 1) {
$pArr = @()
$pi = 0
foreach ($p in $pnList) { $pArr += (Build-TableAxisBlock -node $p -loc "$loc/$idx/point[$pi]"); $pi++ }
$entry['points'] = $pArr
}
$snList = $it.SelectNodes("dcsset:series", $ns)
if ($snList.Count -eq 1) {
$entry['series'] = Build-TableAxisBlock -node $snList[0] -loc "$loc/$idx/series"
} elseif ($snList.Count -gt 1) {
$sArr = @()
$si = 0
foreach ($s in $snList) { $sArr += (Build-TableAxisBlock -node $s -loc "$loc/$idx/series[$si]"); $si++ }
$entry['series'] = $sArr
}
# Selection (chart values) — сохраняем даже [Auto] для bit-perfect presence
$selN = $it.SelectSingleNode("dcsset:selection", $ns)
if ($selN) {
$selI = Build-Selection -selNode $selN -loc "$loc/$idx/selection"
if ($selI.Count -gt 0) { $entry['selection'] = $selI }
}
$opN = $it.SelectSingleNode("dcsset:outputParameters", $ns)
$op = Build-OutputParameters -opNode $opN
if ($op -and $op.Count -gt 0) { $entry['outputParameters'] = $op }
# use=false на самой диаграмме — отключённая ветка
$chUse = Get-Text $it "dcsset:use"
if ($chUse -eq 'false') { $entry['use'] = $false }
# viewMode / userSettingID / userSettingPresentation / itemsViewMode / pointsViewMode / seriesViewMode на chart
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 -not $entry.Contains('viewMode')) { $entry['viewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'userSettingID' -and -not $entry.Contains('userSettingID')) { $entry['userSettingID'] = 'auto' }
elseif ($ch.LocalName -eq 'userSettingPresentation' -and -not $entry.Contains('userSettingPresentation')) {
$uspV = Get-MLText $ch
if ($uspV) { $entry['userSettingPresentation'] = $uspV }
}
elseif ($ch.LocalName -eq 'itemsViewMode' -and -not $entry.Contains('itemsViewMode')) { $entry['itemsViewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'pointsViewMode' -and -not $entry.Contains('pointsViewMode')) { $entry['pointsViewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'seriesViewMode' -and -not $entry.Contains('seriesViewMode')) { $entry['seriesViewMode'] = $ch.InnerText }
}
$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 }
# Block-level viewMode/userSettingID на <dcsset:order>
foreach ($ch in $ordNode.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') { $entry['orderViewMode'] = $ch.InnerText }
elseif ($ch.LocalName -eq 'userSettingID') { $entry['orderUserSettingID'] = 'auto' }
}
}
# 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
}
# True when selection/order is just the single auto element ("Auto") that the
# compiler adds by default to every shorthand group — folding such a group back
# to shorthand is bit-perfect (Parse-StructureShorthand re-adds it on compile).
# Disabled auto ({auto,use}), mixed lists ("Поле","Auto") and explicit fields
# are objects / non-singleton lists and won't match → those keep object form.
function Is-AutoOnly($val) {
if ($null -eq $val) { return $false }
$arr = @($val)
if ($arr.Count -ne 1) { return $false }
return ($arr[0] -is [string]) -and ($arr[0] -eq 'Auto')
}
# 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 filter; selection/order are
# allowed only when they are the default single "Auto" element (see Is-AutoOnly).
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') -and -not (Is-AutoOnly $cur['selection'])) { return $null }
if ($cur.Contains('order') -and -not (Is-AutoOnly $cur['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 }
}
# Fold ТОЛЬКО если companion-имена точно "НачалоПериода"/"КонецПериода" БЕЗ суффикса.
# Иначе compile (который генерирует именно эти имена + type=date + DateFractions=Date)
# не сможет вернуть bit-perfect для отчётов с шаблоном "Период<X>" → "НачалоПериода<X>"/
# "КонецПериода<X>" + DateFractions=DateTime. Оставляем как явные параметры.
# Также НЕ сворачиваем если companions имеют availableAsField=false — compile
# auto-gen не передаёт этот атрибут (ERP-стиль без него; БСП-стиль с ним —
# вариативность не выразима через @autoDates флаг, пусть companions останутся явными).
$beginP = if ($startMatch) { $paramByName[$startMatch] } else { $null }
$endP = if ($endMatch) { $paramByName[$endMatch] } else { $null }
$hasNotAField = ($beginP -and $beginP.notAField) -or ($endP -and $endP.notAField)
if ($startMatch -eq 'НачалоПериода' -and $endMatch -eq 'КонецПериода' -and -not $hasNotAField) {
$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 }
# --- 5e2. fieldTemplates ---
# Привязка <fieldTemplate><field/><template/></fieldTemplate> поля к именованному area-template.
$fieldTemplates = @()
foreach ($ftn in $root.SelectNodes("r:fieldTemplate", $ns)) {
$ftField = Get-Text $ftn "r:field"
$ftTempl = Get-Text $ftn "r:template"
$fieldTemplates += [ordered]@{ field = $ftField; template = $ftTempl }
}
if ($fieldTemplates.Count -gt 0) { $out['fieldTemplates'] = $fieldTemplates }
# --- 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
}
# Block-level userSettingID (direct child of selection/filter/order/conditionalAppearance).
function Get-BlockUSID($node) {
if (-not $node) { return $null }
foreach ($child in $node.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'userSettingID' -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') {
$dExN = $ufItem.SelectSingleNode("dcsset:detailExpression", $ns)
$dEpN = $ufItem.SelectSingleNode("dcsset:detailExpressionPresentation", $ns)
$tExN = $ufItem.SelectSingleNode("dcsset:totalExpression", $ns)
$tEpN = $ufItem.SelectSingleNode("dcsset:totalExpressionPresentation", $ns)
if ($dExN -or $dEpN) {
$d = [ordered]@{}
if ($dExN) { $d['expression'] = $dExN.InnerText }
if ($dEpN) { $d['presentation'] = $dEpN.InnerText }
$entry['detail'] = $d
}
if ($tExN -or $tEpN) {
$t = [ordered]@{}
if ($tExN) { $t['expression'] = $tExN.InnerText }
if ($tEpN) { $t['presentation'] = $tEpN.InnerText }
$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/userSettingID: preserve exact presence (even Normal) for bit-perfect
$svm = Get-BlockVM $selTop
if ($null -ne $svm) { $settings['selectionViewMode'] = $svm }
$susid = Get-BlockUSID $selTop
if ($susid) { $settings['selectionUserSettingID'] = 'auto' }
# 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 }
$fusid = Get-BlockUSID $fTop
if ($fusid) { $settings['filterUserSettingID'] = 'auto' }
# 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 }
$ousid = Get-BlockUSID $ordTop
if ($ousid) { $settings['orderUserSettingID'] = 'auto' }
# 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 }
$causid = Get-BlockUSID $caTop
if ($causid) { $settings['conditionalAppearanceUserSettingID'] = 'auto' }
# 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)")
}
}
}