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