# skd-compile v1.107 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, [string]$Value, [Parameter(Mandatory)] [string]$OutputPath ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Support guard (Ext/ParentConfigurations.bin) --- # See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / # read-only configs unless allowed. Trigger = bin present; reaction from # .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never # throws — guard errors degrade to allow. function Get-RootUuid([string]$xmlPath) { if (-not (Test-Path $xmlPath)) { return $null } try { [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 $el = $mx.DocumentElement.FirstChild while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } } catch {} return $null } function Find-V8Project([string]$startDir) { $d = $startDir for ($i = 0; $i -lt 20 -and $d; $i++) { $pj = Join-Path $d ".v8-project.json" if (Test-Path $pj) { return $pj } $parent = [System.IO.Path]::GetDirectoryName($d) if ($parent -eq $d) { break } $d = $parent } return $null } function Get-EditMode([string]$cfgDir) { try { $pj = Find-V8Project (Get-Location).Path if (-not $pj) { $pj = Find-V8Project $cfgDir } if (-not $pj) { return 'deny' } $proj = Get-Content -Raw $pj | ConvertFrom-Json $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') if ($proj.databases) { foreach ($db in $proj.databases) { if ($db.configSrc) { $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } } } } } if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } return 'deny' } catch { return 'deny' } } function Assert-EditAllowed([string]$targetPath, [string]$require) { try { $rp = $targetPath try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} $elemUuid = Get-RootUuid $rp $cfgDir = $null; $binPath = $null $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } for ($i = 0; $i -lt 12 -and $d; $i++) { if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $cfgDir) { $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } } if ($elemUuid -and $cfgDir) { break } $parent = [System.IO.Path]::GetDirectoryName($d) if ($parent -eq $d) { break } $d = $parent } # New object (no element file): fall back to config root uuid. if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } if (-not $binPath -or -not (Test-Path $binPath)) { return } $bytes = [System.IO.File]::ReadAllBytes($binPath) if ($bytes.Length -le 32) { return } $start = 0 if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') if (-not $hm.Success) { return } $G = [int]$hm.Groups[1].Value $K = [int]$hm.Groups[2].Value if ($K -eq 0) { return } $best = $null if ($elemUuid) { $u = [regex]::Escape($elemUuid.ToLower()) foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { $f1 = [int]$m.Groups[1].Value if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } } } $blocked = $false; $code = ""; $reason = "" if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } elseif ($require -eq 'removed') { if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } } else { if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } } if (-not $blocked) { return } $mode = Get-EditMode $cfgDir if ($mode -eq 'off') { return } # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the # latter throws and would be swallowed by this function's own catch. if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." if ($code -eq "capability-off") { $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." } elseif ($code -eq "not-removed") { $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." } else { $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." } [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") exit 1 } catch { return } } # --- 1. Load and validate JSON --- if ($DefinitionFile -and $Value) { Write-Error "Cannot use both -DefinitionFile and -Value" exit 1 } if (-not $DefinitionFile -and -not $Value) { Write-Error "Either -DefinitionFile or -Value is required" exit 1 } if ($DefinitionFile) { if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile } if (-not (Test-Path $DefinitionFile)) { Write-Error "Definition file not found: $DefinitionFile" exit 1 } $json = Get-Content -Raw -Encoding UTF8 $DefinitionFile } else { $json = $Value } $def = $json | ConvertFrom-Json # --- Sentinel check: refuse to compile if JSON contains skd-decompile sentinels --- # These mark places the decompiler couldn't reverse cleanly; user must resolve # them manually before compile (see .warnings.md alongside the JSON). $script:foundSentinels = @() function Scan-Sentinels { param($obj, [string]$path) if ($null -eq $obj) { return } if ($obj -is [System.Collections.IDictionary]) { foreach ($k in @($obj.Keys)) { if ($k -eq '__unsupported__') { $u = $obj[$k] $id = $u.id; $kind = $u.kind; $loc = $u.loc $script:foundSentinels += " $id [$kind] at $path → $loc" } else { Scan-Sentinels -obj $obj[$k] -path "$path/$k" } } } elseif ($obj -is [System.Management.Automation.PSCustomObject]) { foreach ($p in $obj.PSObject.Properties) { if ($p.Name -eq '__unsupported__') { $u = $p.Value $id = $u.id; $kind = $u.kind; $loc = $u.loc $script:foundSentinels += " $id [$kind] at $path → $loc" } else { Scan-Sentinels -obj $p.Value -path "$path/$($p.Name)" } } } elseif ($obj -is [System.Collections.IEnumerable] -and -not ($obj -is [string])) { $i = 0 foreach ($item in $obj) { Scan-Sentinels -obj $item -path "$path[$i]"; $i++ } } } Scan-Sentinels -obj $def -path '' if ($script:foundSentinels.Count -gt 0) { [Console]::Error.WriteLine("skd-compile: JSON содержит __unsupported__ маркеры от skd-decompile.") [Console]::Error.WriteLine("Это конструкции, которые декомпиляция не смогла обратить — нужно разрешить вручную перед компиляцией.") [Console]::Error.WriteLine("См. .warnings.md рядом с JSON. Найдено:") foreach ($s in $script:foundSentinels) { [Console]::Error.WriteLine($s) } exit 4 } if (-not $def.dataSets -or $def.dataSets.Count -eq 0) { Write-Error "JSON must have at least one entry in 'dataSets'" exit 1 } # Base directory for resolving @file references in query $script:queryBaseDir = if ($DefinitionFile) { [System.IO.Path]::GetDirectoryName($DefinitionFile) } else { (Get-Location).Path } # --- 2. XML helpers --- $script:xml = New-Object System.Text.StringBuilder 16384 function X { param([string]$text) $script:xml.AppendLine($text) | Out-Null } function Esc-Xml { param([string]$s) return $s.Replace('&','&').Replace('<','<').Replace('>','>') } function Resolve-QueryValue { param([string]$val, [string]$baseDir) if (-not $val.StartsWith("@")) { return $val } $filePath = $val.Substring(1) if ([System.IO.Path]::IsPathRooted($filePath)) { $candidates = @($filePath) } else { $candidates = @( (Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath) ) } foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" exit 1 } function Emit-MLText { param([string]$tag, $text, [string]$indent, [switch]$NoXsiType) # Empty value → self-closing tag (matches platform output) if ($null -eq $text -or ($text -is [string] -and $text -eq '')) { if ($NoXsiType) { X "$indent<$tag/>" } else { X "$indent<$tag xsi:type=`"v8:LocalStringType`"/>" } return } if ($NoXsiType) { X "$indent<$tag>" } else { X "$indent<$tag xsi:type=`"v8:LocalStringType`">" } # Multi-lang: object form { ru: "...", en: "..." } → one per language if ($text -is [System.Management.Automation.PSCustomObject] -or $text -is [hashtable] -or $text -is [System.Collections.IDictionary]) { $props = if ($text -is [System.Management.Automation.PSCustomObject]) { $text.PSObject.Properties } else { $text.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } } foreach ($p in $props) { $lang = if ($p -is [hashtable]) { $p.Name } else { $p.Name } $content = if ($p -is [hashtable]) { $p.Value } else { $p.Value } X "$indent`t" X "$indent`t`t$(Esc-Xml "$lang")" X "$indent`t`t$(Esc-Xml "$content")" X "$indent`t" } } else { X "$indent`t" X "$indent`t`tru" X "$indent`t`t$(Esc-Xml "$text")" X "$indent`t" } X "$indent" } function New-Guid-String { return [System.Guid]::NewGuid().ToString() } # --- 3. Resolve defaults --- # DataSources $dataSources = @() if ($def.dataSources) { foreach ($ds in $def.dataSources) { $dataSources += @{ name = "$($ds.name)" type = if ($ds.type) { "$($ds.type)" } else { "Local" } } } } else { $dataSources += @{ name = "ИсточникДанных1"; type = "Local" } } $defaultSource = $dataSources[0].name # Auto-name dataSets $dsIndex = 1 foreach ($ds in $def.dataSets) { if (-not $ds.name) { $ds | Add-Member -NotePropertyName "name" -NotePropertyValue "НаборДанных$dsIndex" -Force } $dsIndex++ } # --- 4. Type system --- # Type synonyms — normalize Russian/common names to canonical DSL types # Use case-sensitive hashtable to avoid PS 5.1 DuplicateKeyInHashLiteral $script:typeSynonyms = New-Object System.Collections.Hashtable # Russian names (case doesn't matter — we'll also do case-insensitive lookup) $script:typeSynonyms["число"] = "decimal" $script:typeSynonyms["строка"] = "string" $script:typeSynonyms["булево"] = "boolean" $script:typeSynonyms["дата"] = "date" $script:typeSynonyms["датавремя"] = "dateTime" $script:typeSynonyms["время"] = "time" $script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" # English canonical (lowercase for lookup) $script:typeSynonyms["bool"] = "boolean" $script:typeSynonyms["str"] = "string" $script:typeSynonyms["int"] = "decimal" $script:typeSynonyms["integer"] = "decimal" $script:typeSynonyms["number"] = "decimal" $script:typeSynonyms["num"] = "decimal" # Reference synonyms (Russian, lowercase) $script:typeSynonyms["справочникссылка"] = "CatalogRef" $script:typeSynonyms["документссылка"] = "DocumentRef" $script:typeSynonyms["перечислениессылка"] = "EnumRef" $script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" $script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" function Resolve-TypeStr { param([string]$typeStr) if (-not $typeStr) { return $typeStr } # Check for parameterized types: число(15,2), строка(100), etc. if ($typeStr -match '^([^(]+)\((.+)\)$') { $baseName = $Matches[1].Trim() $params = $Matches[2] # Resolve base name (case-insensitive via .ToLower()) $resolved = $script:typeSynonyms[$baseName.ToLower()] if ($resolved) { return "$resolved($params)" } return $typeStr } # Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации if ($typeStr.Contains('.')) { $dotIdx = $typeStr.IndexOf('.') $prefix = $typeStr.Substring(0, $dotIdx) $suffix = $typeStr.Substring($dotIdx) # includes the dot $resolved = $script:typeSynonyms[$prefix.ToLower()] if ($resolved) { return "$resolved$suffix" } return $typeStr } # Simple name lookup (case-insensitive) $resolved = $script:typeSynonyms[$typeStr.ToLower()] if ($resolved) { return $resolved } return $typeStr } function Emit-ValueType { param($typeStr, [string]$indent) if (-not $typeStr) { return } # Multi-type: iterate and emit each type with its qualifiers if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { foreach ($t in $typeStr) { Emit-SingleValueType -typeStr "$t" -indent $indent } return } Emit-SingleValueType -typeStr "$typeStr" -indent $indent } function Emit-SingleValueType { param([string]$typeStr, [string]$indent) if (-not $typeStr) { return } # Resolve synonyms first $typeStr = Resolve-TypeStr $typeStr # boolean if ($typeStr -eq "boolean") { X "$indentxs:boolean" return } # string, string(N), string(N,fix) — fix → AllowedLength=Fixed if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { $len = if ($Matches[2]) { $Matches[2] } else { "0" } $al = if ($Matches[4]) { "Fixed" } else { "Variable" } X "$indentxs:string" X "$indent" X "$indent`t$len" X "$indent`t$al" X "$indent" return } # decimal forms (defaults — bare decimal = money 10,2; decimal(N) = integer N,0): # decimal → 10,2,Any # decimal(N) → N,0,Any # decimal(N,nonneg) → N,0,Nonnegative # decimal(N,M) → N,M,Any # decimal(N,M,nonneg) → N,M,Nonnegative if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { if (-not $Matches[1]) { $digits = "10"; $fraction = "2"; $sign = "Any" } else { $digits = $Matches[2] $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } $sign = if ($Matches[5]) { "Nonnegative" } else { "Any" } } X "$indentxs:decimal" X "$indent" X "$indent`t$digits" X "$indent`t$fraction" X "$indent`t$sign" X "$indent" return } # date / dateTime / time — all use xs:dateTime, differ only in DateFractions if ($typeStr -match '^(date|dateTime|time)$') { $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } "time" { "Time" } } X "$indentxs:dateTime" X "$indent" X "$indent`t$fractions" X "$indent" return } # StandardPeriod if ($typeStr -eq "StandardPeriod") { X "$indentv8:StandardPeriod" return } # Reference types: CatalogRef.XXX, DocumentRef.XXX, EnumRef.XXX, etc. # Real DCS files use inline namespace d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config" if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { X "$indentd5p1:$(Esc-Xml $typeStr)" return } # TypeSet (композитный тип-набор): голое имя без точки, типа DocumentRef / CatalogRef / # EnumRef / ChartOfAccountsRef / etc. (все ссылки указанного класса). # Эмитим вместо . if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef|InformationRegisterRef|AnyRef)$') { X "$indentd5p1:$(Esc-Xml $typeStr)" return } # Fallback — assume dot-qualified types are also config references if ($typeStr.Contains('.')) { X "$indentd5p1:$(Esc-Xml $typeStr)" return } X "$indent$(Esc-Xml $typeStr)" } # --- 5. Field shorthand parser --- function Parse-FieldShorthand { param([string]$s) $result = @{ dataPath = ""; field = ""; title = ""; type = "" roles = @(); restrict = @(); appearance = [ordered]@{} roleExtras = [ordered]@{} } # Extract @roles $roleMatches = [regex]::Matches($s, '@(\w+)') foreach ($m in $roleMatches) { $result.roles += $m.Groups[1].Value } $s = [regex]::Replace($s, '\s*@\w+', '') # Extract #restrictions $restrictMatches = [regex]::Matches($s, '#(\w+)') foreach ($m in $restrictMatches) { $result.restrict += $m.Groups[1].Value } $s = [regex]::Replace($s, '\s*#\w+', '') # Extract role kv=value (e.g. balanceGroupName=Сумма balanceType=OpeningBalance) $kvMatches = [regex]::Matches($s, '(\w+)=(\S+)') foreach ($m in $kvMatches) { $result.roleExtras[$m.Groups[1].Value] = $m.Groups[2].Value } $s = [regex]::Replace($s, '\s*\w+=\S+', '') # Split name: type $s = $s.Trim() if ($s.Contains(':')) { $parts = $s -split ':', 2 $result.dataPath = $parts[0].Trim() $result.type = Resolve-TypeStr ($parts[1].Trim()) } else { $result.dataPath = $s } $result.field = $result.dataPath return $result } # Universal role spec parser: string / array / object / null # Returns @{ tokens = @(...); extras = [ordered]@{...} } function Parse-RoleSpec { param($spec) $tokens = @() $extras = [ordered]@{} if ($null -ne $spec) { if ($spec -is [string]) { if ($spec -notmatch '\s' -and $spec -notmatch '=') { $tokens += $spec } else { $s = $spec.Trim() foreach ($m in [regex]::Matches($s, '@(\w+)')) { $tokens += $m.Groups[1].Value } $s = [regex]::Replace($s, '\s*@\w+', '').Trim() foreach ($m in [regex]::Matches($s, '(\w+)=(\S+)')) { $extras[$m.Groups[1].Value] = $m.Groups[2].Value } } } elseif ($spec -is [array] -or $spec -is [System.Collections.IList]) { foreach ($t in $spec) { $tokens += "$t" } } elseif ($spec.PSObject -and $spec.PSObject.Properties) { foreach ($prop in $spec.PSObject.Properties) { $val = $prop.Value if ($val -is [bool]) { if ($val) { $tokens += $prop.Name } } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) { $extras[$prop.Name] = "$val" } } } elseif ($spec -is [hashtable] -or $spec -is [System.Collections.IDictionary]) { foreach ($k in $spec.Keys) { $val = $spec[$k] if ($val -is [bool]) { if ($val) { $tokens += "$k" } } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) { $extras["$k"] = "$val" } } } } # Deprecated alias: balanceGroup → balanceGroupName (старое имя в коде compile, в реальном XML — Name) if ($extras.Contains('balanceGroup') -and -not $extras.Contains('balanceGroupName')) { $extras['balanceGroupName'] = $extras['balanceGroup'] $extras.Remove('balanceGroup') } return @{ tokens = $tokens; extras = $extras } } # --- 6. Total field shorthand parser --- function Parse-TotalShorthand { param([string]$s) # "DataPath: Func" or "DataPath: Func(expr)" $parts = $s -split ':', 2 $dataPath = $parts[0].Trim() $funcPart = $parts[1].Trim() # Known DCS aggregate functions (ru + en) $aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее', 'Sum','Count','Min','Max','Avg', 'Minimum','Maximum','Average') if ($funcPart -match '^\w+\(') { # Already has expression form: Func(expr) return @{ dataPath = $dataPath; expression = $funcPart } } elseif ($funcPart -in $aggFuncs) { # Short: Func → Func(DataPath) return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } } else { # Identity or custom expression — use as-is return @{ dataPath = $dataPath; expression = $funcPart } } } # --- 7. Parameter shorthand parser --- function Split-ValueListCsv { # Split on top-level commas (respecting 'single'/"double" quotes), strip quotes, # drop empties. No ':' handling — values may contain colons (dateTime). param([string]$s) $result = @() if ($null -eq $s) { return ,$result } $items = @() $buf = New-Object System.Text.StringBuilder $inQuote = $null for ($i = 0; $i -lt $s.Length; $i++) { $ch = $s[$i] if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } } elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) } elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() } else { [void]$buf.Append($ch) } } if ($buf.Length -gt 0) { $items += $buf.ToString() } foreach ($raw in $items) { $t = $raw.Trim() if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) } if ($t -ne "") { $result += $t } } return ,$result } function Parse-ParamShorthand { param([string]$s) $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null } # Extract @autoDates flag if ($s -match '@autoDates') { $result.autoDates = $true $s = $s -replace '\s*@autoDates', '' } # Extract @valueList flag if ($s -match '@valueList') { $result.valueListAllowed = $true $s = $s -replace '\s*@valueList', '' } # Extract @hidden flag if ($s -match '@hidden') { $result.hidden = $true $s = $s -replace '\s*@hidden', '' } # Extract optional [Title] (mirrors Parse-FieldShorthand) if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim() $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty value if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { $result.name = $Matches[1].Trim() $result.type = Resolve-TypeStr ($Matches[2].Trim()) if ($Matches[4]) { $rhs = $Matches[4].Trim() $items = Split-ValueListCsv $rhs if ($items.Count -ge 2) { # Multi-value default → list; valueListAllowed implied $result.value = $items $result.valueListAllowed = $true } elseif ($items.Count -eq 1) { $result.value = $items[0] } else { $result.value = $rhs } } } else { $result.name = $s.Trim() } return $result } # --- 8. Calculated field shorthand parser --- function Parse-CalcShorthand { param([string]$s) # Pattern: "Name [Title]: type = Expression #noField #noFilter ...". # - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside # an expression (e.g. index access) isn't interpreted as a title. # - `#restrict` flags use a known-names pattern and are extracted globally — # the docs put them after `=`, and the closed flag set avoids matching # `#word` that happens to appear inside a string literal. $restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b' $restrict = @() foreach ($m in [regex]::Matches($s, $restrictPattern)) { $restrict += $m.Groups[1].Value } $s = [regex]::Replace($s, "\s*$restrictPattern", '') $eqIdx = $s.IndexOf('=') if ($eqIdx -gt 0) { $lhs = $s.Substring(0, $eqIdx) $rhs = $s.Substring($eqIdx + 1).Trim() } else { $lhs = $s $rhs = "" } $title = "" if ($lhs -match '\[([^\]]+)\]') { $title = $Matches[1] $lhs = $lhs -replace '\s*\[[^\]]+\]', '' } $lhs = $lhs.Trim() $type = "" $dataPath = $lhs if ($lhs.Contains(':')) { $parts = $lhs -split ':', 2 $dataPath = $parts[0].Trim() $type = Resolve-TypeStr ($parts[1].Trim()) } return @{ dataPath = $dataPath expression = $rhs type = $type title = $title restrict = $restrict } } # --- 8b. DataParameter shorthand parser --- # Formats: "Период = LastMonth @user", "Организация @off @user", "Период @user" function Parse-DataParamShorthand { param([string]$s) $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } # Extract @flags if ($s -match '@user') { $result.userSettingID = "auto" $s = $s -replace '\s*@user', '' } if ($s -match '@off') { $result.use = $false $s = $s -replace '\s*@off', '' } if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess" $s = $s -replace '\s*@quickAccess', '' } if ($s -match '@normal') { $result.viewMode = "Normal" $s = $s -replace '\s*@normal', '' } $s = $s.Trim() # Split "Name = Value" if ($s -match '^([^=]+)=\s*(.+)$') { $result.parameter = $Matches[1].Trim() $valStr = $Matches[2].Trim() # Detect StandardPeriod variants $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") if ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valStr } elseif ($valStr -eq "true" -or $valStr -eq "false") { $result.value = [bool]($valStr -eq "true") } else { $result.value = $valStr } } else { $result.parameter = $s } return $result } # --- 8c. Filter item shorthand parser --- # Formats: "Организация = _ @off @user", "Дата >= 2024-01-01T00:00:00", "Статус filled" function Parse-FilterShorthand { param([string]$s) $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null } # Extract @flags if ($s -match '@user') { $result.userSettingID = "auto" $s = $s -replace '\s*@user', '' } if ($s -match '@off') { $result.use = $false $s = $s -replace '\s*@off', '' } if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess" $s = $s -replace '\s*@quickAccess', '' } if ($s -match '@normal') { $result.viewMode = "Normal" $s = $s -replace '\s*@normal', '' } if ($s -match '@inaccessible') { $result.viewMode = "Inaccessible" $s = $s -replace '\s*@inaccessible', '' } $s = $s.Trim() # Try to match: Field op Value, or Field op (no value for filled/notFilled) # Operators sorted longest first to match >= before > $opPatterns = @('<>', '>=', '<=', '=', '>', '<', 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', 'notFilled\b', 'filled\b') $opJoined = $opPatterns -join '|' if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { $result.field = $Matches[1].Trim() $opRaw = $Matches[2].Trim() $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } # Map op $opMap = @{ "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" "contains" = "Contains"; "notContains" = "NotContains" "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" "filled" = "Filled"; "notFilled" = "NotFilled" } $mapped = $opMap[$opRaw] if ($mapped) { $result.op = $opRaw } else { $result.op = $opRaw } # Parse value (skip "_" which means empty/placeholder) if ($valPart -and $valPart -ne "_") { if ($valPart -eq "true" -or $valPart -eq "false") { $result.value = [bool]($valPart -eq "true") $result["valueType"] = "xs:boolean" } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valPart $result["valueType"] = "xs:dateTime" } elseif ($valPart -match '^\d+(\.\d+)?$') { $result.value = $valPart $result["valueType"] = "xs:decimal" } elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart $result["valueType"] = "dcscor:DesignTimeValue" } else { $result.value = $valPart $result["valueType"] = "xs:string" } } } else { # No operator found — just a field name $result.field = $s } return $result } # --- 9. Comparison type mapper --- $script:comparisonTypes = @{ "=" = "Equal"; "<>" = "NotEqual" ">" = "Greater"; ">=" = "GreaterOrEqual" "<" = "Less"; "<=" = "LessOrEqual" "in" = "InList"; "notIn" = "NotInList" "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" "contains" = "Contains"; "notContains" = "NotContains" "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" "filled" = "Filled"; "notFilled" = "NotFilled" } # --- 10. Output parameter type detection --- $script:outputParamTypes = @{ "Заголовок" = "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" } # --- 11. Emit sections --- # === DataSources === function Emit-DataSources { foreach ($ds in $dataSources) { X "`t" X "`t`t$(Esc-Xml $ds.name)" X "`t`t$(Esc-Xml $ds.type)" X "`t" } } # === Fields === function Has-JsonProp { param($obj, [string]$name) if ($null -eq $obj) { return $false } if ($obj.PSObject -and $obj.PSObject.Properties) { return $null -ne $obj.PSObject.Properties[$name] } if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) } return $false } function Emit-InputParameters { param($ip, [string]$indent) if ($null -eq $ip) { return } $items = @($ip) if ($items.Count -eq 0) { return } X "$indent" foreach ($item in $items) { X "$indent`t" if ((Has-JsonProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`tfalse" } X "$indent`t`t$(Esc-Xml "$($item.parameter)")" if (Has-JsonProp $item 'choiceParameters') { $cp = $item.choiceParameters $cpItems = if ($null -ne $cp) { @($cp) } else { @() } if ($cpItems.Count -eq 0) { X "$indent`t`t" } else { X "$indent`t`t" foreach ($cpItem in $cpItems) { X "$indent`t`t`t" X "$indent`t`t`t`t$(Esc-Xml "$($cpItem.name)")" foreach ($v in @($cpItem.values)) { if ($v -is [bool]) { $vStr = if ($v) { 'true' } else { 'false' } X "$indent`t`t`t`t$vStr" } elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t$v" } else { X "$indent`t`t`t`t$(Esc-Xml "$v")" } } X "$indent`t`t`t" } X "$indent`t`t" } } elseif (Has-JsonProp $item 'choiceParameterLinks') { $cpl = $item.choiceParameterLinks $cplItems = if ($null -ne $cpl) { @($cpl) } else { @() } if ($cplItems.Count -eq 0) { X "$indent`t`t" } else { X "$indent`t`t" foreach ($cplItem in $cplItems) { X "$indent`t`t`t" X "$indent`t`t`t`t$(Esc-Xml "$($cplItem.name)")" X "$indent`t`t`t`t$(Esc-Xml "$($cplItem.value)")" $mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' } X "$indent`t`t`t`t$mode" X "$indent`t`t`t" } X "$indent`t`t" } } elseif (Has-JsonProp $item 'value') { # Simple typed value — определяем xsi:type из JSON-типа $val = $item.value # Явный кастомный type из decompile: {uri, name} → $customType = $null if (Has-JsonProp $item 'valueType') { $vtSrc = $item.valueType $uri = $null; $tName = $null if ($vtSrc -is [PSCustomObject]) { if ($vtSrc.PSObject.Properties['uri']) { $uri = "$($vtSrc.uri)" } if ($vtSrc.PSObject.Properties['name']) { $tName = "$($vtSrc.name)" } } elseif ($vtSrc -is [System.Collections.IDictionary]) { if ($vtSrc.Contains('uri')) { $uri = "$($vtSrc['uri'])" } if ($vtSrc.Contains('name')) { $tName = "$($vtSrc['name'])" } } if ($uri -and $tName) { $customType = @{ uri = $uri; name = $tName } } } if ($customType) { X "$indent`t`t$(Esc-Xml "$val")" } elseif ($val -is [bool]) { $vStr = if ($val) { 'true' } else { 'false' } X "$indent`t`t$vStr" } elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t$val" } elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { # Multilang dict {ru, en, ...} → LocalStringType Emit-MLText -tag "dcscor:value" -text $val -indent "$indent`t`t" } else { X "$indent`t`t$(Esc-Xml "$val")" } } X "$indent`t" } X "$indent" } function Emit-Field { param($fieldDef, [string]$indent) if ($fieldDef -is [string]) { $f = Parse-FieldShorthand $fieldDef } else { $f = @{ dataPath = if ($fieldDef.dataPath) { "$($fieldDef.dataPath)" } elseif ($fieldDef.field) { "$($fieldDef.field)" } else { "" } field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" } title = if ($fieldDef.title) { $fieldDef.title } else { "" } type = if ($fieldDef.type) { if ($fieldDef.type -is [array] -or $fieldDef.type -is [System.Collections.IList]) { @($fieldDef.type | ForEach-Object { Resolve-TypeStr "$_" }) } else { Resolve-TypeStr "$($fieldDef.type)" } } else { "" } roles = @() restrict = @() appearance = [ordered]@{} roleExtras = [ordered]@{} } # Parse role (string shorthand / array / object — единый формат с /skd-edit set-field-role) if ($fieldDef.role) { $parsed = Parse-RoleSpec $fieldDef.role $f.roles = $parsed.tokens $f.roleExtras = $parsed.extras } # Parse restrictions if ($fieldDef.restrict) { $f.restrict = @($fieldDef.restrict) } # Parse appearance (сохраняем значение как есть — может быть string или multilang dict) if ($fieldDef.appearance) { foreach ($prop in $fieldDef.appearance.PSObject.Properties) { $f.appearance[$prop.Name] = $prop.Value } } if ($fieldDef.presentationExpression) { $f["presentationExpression"] = "$($fieldDef.presentationExpression)" } # attrRestrict if ($fieldDef.attrRestrict) { $f["attrRestrict"] = @($fieldDef.attrRestrict) } # availableValues — array of {value, presentation} if ($fieldDef.availableValues) { $f["availableValues"] = $fieldDef.availableValues } # orderExpression — {expression, orderType, autoOrder} if ($fieldDef.orderExpression) { $f["orderExpression"] = $fieldDef.orderExpression } # inputParameters — массив элементов, типизированных по форме value if ($null -ne $fieldDef.inputParameters) { $f["inputParameters"] = $fieldDef.inputParameters } # folder: true → DataSetFieldFolder (поле-папка для UI-группировки, только dataPath+title) if ($fieldDef.folder -eq $true) { $f["folder"] = $true } } # DataSetFieldFolder — только dataPath + title (для UI-группировки полей в композиторе) if ($f["folder"]) { X "$indent" X "$indent`t$(Esc-Xml $f.dataPath)" if ($f.title) { Emit-MLText -tag "title" -text $f.title -indent "$indent`t" } X "$indent" return } X "$indent" X "$indent`t$(Esc-Xml $f.dataPath)" X "$indent`t$(Esc-Xml $f.field)" # Title if ($f.title) { Emit-MLText -tag "title" -text $f.title -indent "$indent`t" } # UseRestriction $restrictMap = @{ "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" "noGroup" = "group"; "noOrder" = "order" } if ($f.restrict.Count -gt 0) { X "$indent`t" foreach ($r in $f.restrict) { $xmlName = $restrictMap["$r"] if ($xmlName) { X "$indent`t`t<$xmlName>true" } } X "$indent`t" } # AttributeUseRestriction if ($f["attrRestrict"] -and $f["attrRestrict"].Count -gt 0) { X "$indent`t" foreach ($r in $f["attrRestrict"]) { $xmlName = $restrictMap["$r"] if ($xmlName) { X "$indent`t`t<$xmlName>true" } } X "$indent`t" } # Role $hasExtras = $f["roleExtras"] -and $f["roleExtras"].Count -gt 0 if ($f.roles.Count -gt 0 -or $hasExtras) { X "$indent`t" foreach ($role in $f.roles) { if ($role -eq "period") { # @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить. $pnInExtras = $hasExtras -and $f["roleExtras"].Contains('periodNumber') $ptInExtras = $hasExtras -and $f["roleExtras"].Contains('periodType') if (-not $pnInExtras) { X "$indent`t`t1" } if (-not $ptInExtras) { X "$indent`t`tMain" } } else { X "$indent`t`ttrue" } } if ($hasExtras) { foreach ($k in $f["roleExtras"].Keys) { X "$indent`t`t$(Esc-Xml "$($f["roleExtras"][$k])")" } } X "$indent`t" } # OrderExpression — после role, до valueType. Допустим массив (multi-sort). if ($f["orderExpression"]) { $oeRaw = $f["orderExpression"] if ($oeRaw -is [System.Collections.IDictionary]) { $oeList = @($oeRaw) } elseif ($oeRaw -is [System.Collections.IList]) { $oeList = $oeRaw } else { $oeList = @($oeRaw) } foreach ($oe in $oeList) { $expr = if ($oe.expression) { "$($oe.expression)" } else { '' } $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' } $autoOrder = if ($null -ne $oe.autoOrder) { $(if ($oe.autoOrder) { 'true' } else { 'false' }) } else { 'false' } X "$indent`t" X "$indent`t`t$(Esc-Xml $expr)" X "$indent`t`t$oType" X "$indent`t`t$autoOrder" X "$indent`t" } } # ValueType if ($f.type) { X "$indent`t" Emit-ValueType -typeStr $f.type -indent "$indent`t`t" X "$indent`t" } # AvailableValues — list of allowed values with optional multilang presentation if ($f["availableValues"]) { foreach ($av in $f["availableValues"]) { X "$indent`t" $avVal = $av.value $avType = if ($av.valueType) { "$($av.valueType)" } else { '' } if (-not $avType) { if ($avVal -is [bool]) { $avType = 'xs:boolean' } elseif ($avVal -is [int] -or $avVal -is [long] -or $avVal -is [double]) { $avType = 'xs:decimal' } elseif ("$avVal" -match '^\d{4}-\d{2}-\d{2}T') { $avType = 'xs:dateTime' } else { $avType = 'xs:string' } } $avStr = if ($avVal -is [bool]) { "$avVal".ToLower() } else { Esc-Xml "$avVal" } X "$indent`t`t$avStr" if ($av.presentation) { Emit-MLText -tag "presentation" -text $av.presentation -indent "$indent`t`t" } X "$indent`t" } } # Appearance if ($f.appearance -and $f.appearance.Count -gt 0) { X "$indent`t" foreach ($key in $f.appearance.Keys) { $val = $f.appearance[$key] # ГоризонтальноеПоложение требует специального xsi:type (v8ui:HorizontalAlign), не строка if ($key -eq "ГоризонтальноеПоложение" -and -not ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject])) { X "$indent`t`t" X "$indent`t`t`t$(Esc-Xml $key)" X "$indent`t`t`t$(Esc-Xml "$val")" X "$indent`t`t" } else { Emit-AppearanceValue -key $key -val $val -indent "$indent`t`t" } } X "$indent`t" } # PresentationExpression if ($f["presentationExpression"]) { X "$indent`t$(Esc-Xml $f["presentationExpression"])" } # InputParameters — в конце field if ($f["inputParameters"]) { Emit-InputParameters -ip $f["inputParameters"] -indent "$indent`t" } X "$indent" } # === DataSets === function Emit-DataSet { param($ds, [string]$indent, [string]$tagName = "dataSet") # Determine type if ($ds.items) { $dsType = "DataSetUnion" } elseif ($ds.objectName) { $dsType = "DataSetObject" } else { $dsType = "DataSetQuery" } X "$indent<$tagName xsi:type=`"$dsType`">" X "$indent`t$(Esc-Xml "$($ds.name)")" # Fields if ($ds.fields) { foreach ($f in $ds.fields) { Emit-Field -fieldDef $f -indent "$indent`t" } } # DataSource (not for Union) if ($dsType -ne "DataSetUnion") { $src = if ($ds.source) { "$($ds.source)" } else { $defaultSource } X "$indent`t$(Esc-Xml $src)" } # Type-specific content if ($dsType -eq "DataSetQuery") { $queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir X "$indent`t$(Esc-Xml $queryText)" if ($ds.autoFillFields -eq $false) { X "$indent`tfalse" } } elseif ($dsType -eq "DataSetObject") { X "$indent`t$(Esc-Xml "$($ds.objectName)")" } elseif ($dsType -eq "DataSetUnion") { foreach ($item in $ds.items) { # Union inner items are wrapped as Emit-DataSet -ds $item -indent "$indent`t" -tagName "item" | Out-Null } } X "$indent" } function Emit-DataSets { foreach ($ds in $def.dataSets) { Emit-DataSet -ds $ds -indent "`t" } } # === DataSetLinks === function Emit-DataSetLinks { if (-not $def.dataSetLinks) { return } foreach ($link in $def.dataSetLinks) { X "`t" $srcDS = if ($link.source) { "$($link.source)" } elseif ($link.sourceDataSet) { "$($link.sourceDataSet)" } else { "" } $dstDS = if ($link.dest) { "$($link.dest)" } elseif ($link.destinationDataSet) { "$($link.destinationDataSet)" } else { "" } $srcEx = if ($link.sourceExpr) { "$($link.sourceExpr)" } elseif ($link.sourceExpression) { "$($link.sourceExpression)" } else { "" } $dstEx = if ($link.destExpr) { "$($link.destExpr)" } elseif ($link.destinationExpression) { "$($link.destinationExpression)" } else { "" } X "`t`t$(Esc-Xml $srcDS)" X "`t`t$(Esc-Xml $dstDS)" X "`t`t$(Esc-Xml $srcEx)" X "`t`t$(Esc-Xml $dstEx)" if ($link.parameter) { X "`t`t$(Esc-Xml "$($link.parameter)")" } if ($link.PSObject.Properties.Match('parameterListAllowed').Count -gt 0 -and $link.parameterListAllowed) { X "`t`ttrue" } if ($link.PSObject.Properties.Match('startExpression').Count -gt 0 -and $null -ne $link.startExpression) { X "`t`t$(Esc-Xml "$($link.startExpression)")" } if ($link.PSObject.Properties.Match('linkConditionExpression').Count -gt 0 -and $null -ne $link.linkConditionExpression) { X "`t`t$(Esc-Xml "$($link.linkConditionExpression)")" } X "`t" } } # === CalculatedFields === function Emit-CalcFields { if (-not $def.calculatedFields) { return } $restrictMap = @{ "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" "noGroup" = "group"; "noOrder" = "order" } foreach ($cf in $def.calculatedFields) { # Collect dataPath/expression/title/type/restrict/appearance from either # shorthand string or object form. Object form accepts dataPath/field/name # as synonyms; useRestriction/restrict accepts object, array, or flag string. $title = "" $typeStr = "" $restrictTokens = @() $restrictObj = $null $appearance = $null if ($cf -is [string]) { $parsed = Parse-CalcShorthand $cf $dataPath = "$($parsed.dataPath)" $expression = "$($parsed.expression)" $title = $parsed.title $typeStr = "$($parsed.type)" if ($parsed.restrict) { $restrictTokens = @($parsed.restrict) } } else { $dataPath = if ($cf.dataPath) { "$($cf.dataPath)" } elseif ($cf.field) { "$($cf.field)" } else { "$($cf.name)" } $expression = "$($cf.expression)" if ($cf.title) { $title = $cf.title } if ($cf.type) { $typeStr = Resolve-TypeStr "$($cf.type)" } $restrictVal = if ($cf.restrict) { $cf.restrict } elseif ($cf.useRestriction) { $cf.useRestriction } else { $null } if ($restrictVal) { if ($restrictVal -is [System.Management.Automation.PSCustomObject] -or $restrictVal -is [hashtable]) { $restrictObj = $restrictVal } elseif ($restrictVal -is [string]) { # Flag-string form: "#noField #noFilter #noGroup #noOrder" (or without `#`) foreach ($tok in ($restrictVal -split '\s+')) { $t = $tok.Trim().TrimStart('#') if ($t) { $restrictTokens += $t } } } else { # Array form: ["noField", "noFilter", ...] foreach ($r in $restrictVal) { $restrictTokens += "$r" } } } if ($cf.appearance) { $appearance = $cf.appearance } } X "`t" X "`t`t$(Esc-Xml $dataPath)" X "`t`t$(Esc-Xml $expression)" if ($title) { Emit-MLText -tag "title" -text $title -indent "`t`t" } if ($typeStr) { X "`t`t" Emit-ValueType -typeStr $typeStr -indent "`t`t`t" X "`t`t" } if ($restrictObj -or $restrictTokens.Count -gt 0) { X "`t`t" if ($restrictObj) { foreach ($prop in $restrictObj.PSObject.Properties) { if ($prop.Value -eq $true) { X "`t`t`t<$($prop.Name)>true" } } } else { foreach ($r in $restrictTokens) { $xmlName = $restrictMap["$r"] if ($xmlName) { X "`t`t`t<$xmlName>true" } } } X "`t`t" } if ($appearance) { X "`t`t" foreach ($prop in $appearance.PSObject.Properties) { # ГоризонтальноеПоложение — особый xsi:type (если не multilang) if ($prop.Name -eq "ГоризонтальноеПоложение" -and -not ($prop.Value -is [hashtable] -or $prop.Value -is [System.Collections.IDictionary] -or $prop.Value -is [PSCustomObject])) { X "`t`t`t" X "`t`t`t`t$(Esc-Xml $prop.Name)" X "`t`t`t`t$(Esc-Xml "$($prop.Value)")" X "`t`t`t" } else { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "`t`t`t" } } X "`t`t" } X "`t" } } # === TotalFields === function Emit-TotalFields { if (-not $def.totalFields) { return } foreach ($tf in $def.totalFields) { if ($tf -is [string]) { $parsed = Parse-TotalShorthand $tf } else { $parsed = @{ dataPath = "$($tf.dataPath)" expression = "$($tf.expression)" } if ($tf.group) { $parsed.groups = @($tf.group) } } X "`t" X "`t`t$(Esc-Xml $parsed.dataPath)" X "`t`t$(Esc-Xml $parsed.expression)" if ($parsed.groups) { foreach ($g in $parsed.groups) { X "`t`t$(Esc-Xml "$g")" } } X "`t" } } # === Parameters === function Emit-SingleParam { param($p, $parsed) X "`t" X "`t`t$(Esc-Xml $parsed.name)" # Title (from parsed first, then from object form; accept `presentation` as # a synonym — 1C UI labels a parameter's caption "Представление"). $title = "" if ($parsed.title) { $title = $parsed.title } elseif ($p -isnot [string] -and $p.title) { $title = $p.title } elseif ($p -isnot [string] -and $p.presentation) { $title = $p.presentation } if ($title) { Emit-MLText -tag "title" -text $title -indent "`t`t" } # ValueType if ($parsed.type) { X "`t`t" Emit-ValueType -typeStr $parsed.type -indent "`t`t`t" X "`t`t" } # Value — for valueListAllowed params Designer omits when empty $vla = [bool]$parsed.valueListAllowed # Multi-value (массив значений по умолчанию для valueListAllowed-параметра) — эмитим # каждый отдельным . Различаем массив значений от composite type (тоже array, # но в parsed.type). $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) if ($parsed.type -is [array] -or $parsed.type -is [System.Collections.IList]) { # Composite type — Designer writes xsi:nil for any empty composite; # non-empty composite values are uncommon and would need per-type tagging. if (Test-EmptyValue $parsed.value) { if (-not $vla) { X "`t`t" } } } elseif ($parsed.nilValue -eq $true) { # Принудительный xsi:nil даже когда тип известен (для bit-perfect round-trip). if (-not $vla) { X "`t`t" } } elseif ($valIsArray) { foreach ($v in @($parsed.value)) { Emit-ParamValue -type $parsed.type -val $v -indent "`t`t" -valueListAllowed $false } } else { Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" -valueListAllowed $vla } # Hidden implies useRestriction=true + availableAsField=false if ($parsed.hidden -eq $true) { $parsed.availableAsField = $false $parsed.useRestriction = $true } # UseRestriction — платформа всегда эмитит этот тег у параметра (true/false) $urEmit = $false if ($parsed.useRestriction -eq $true) { $urEmit = $true } elseif ($p -isnot [string] -and $p.useRestriction -eq $true) { $urEmit = $true } X ("`t`t" + $(if ($urEmit) { 'true' } else { 'false' }) + "") # Expression if ($parsed.expression) { X "`t`t$(Esc-Xml $parsed.expression)" } # AvailableAsField if ($parsed.availableAsField -eq $false) { X "`t`tfalse" } # ValueListAllowed if ($parsed.valueListAllowed -eq $true) { X "`t`ttrue" } # AvailableValues if ($p -isnot [string] -and $p.availableValues) { foreach ($av in $p.availableValues) { X "`t`t" if (Test-EmptyValue $av.value) { Emit-EmptyValue -type $parsed.type -indent "`t`t`t" -tagPrefix "" -valueListAllowed $false } else { $av_v = $av.value if ($av_v -is [bool]) { $bv = "$av_v".ToLower() X "`t`t`t$bv" } elseif ($av_v -is [int] -or $av_v -is [long] -or $av_v -is [double]) { X "`t`t`t$av_v" } else { $avVal = "$av_v" $avType = "xs:string" if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $avType = "dcscor:DesignTimeValue" } X "`t`t`t$(Esc-Xml $avVal)" } } # `title` accepted as synonym of `presentation` — both map to the same UI label. $avPres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { "" } if ($avPres) { Emit-MLText -tag "presentation" -text $avPres -indent "`t`t`t" } X "`t`t" } } # DenyIncompleteValues $deny = $parsed.denyIncompleteValues -eq $true -or ( $null -ne $p -and $p -isnot [string] -and $p.denyIncompleteValues -eq $true) if ($deny) { X "`t`ttrue" } # Use — object form wins, else parsed (set by @autoDates default) $useVal = $null if ($null -ne $p -and $p -isnot [string] -and $p.use) { $useVal = "$($p.use)" } elseif ($parsed.use) { $useVal = "$($parsed.use)" } if ($useVal) { X "`t`t$(Esc-Xml $useVal)" } # InputParameters на параметре (ФорматРедактирования и т.п.) if ($null -ne $p -and $p -isnot [string] -and $p.inputParameters) { Emit-InputParameters -ip $p.inputParameters -indent "`t`t" } X "`t" } $script:allParams = @() function Emit-Parameters { if (-not $def.parameters) { return } foreach ($p in $def.parameters) { if ($p -is [string]) { $parsed = Parse-ParamShorthand $p } else { # Composite type: ["string(10,fix)", "CatalogRef.X"] → array of resolved # strings; emit-valueType handles arrays, empty value falls through to nil. $resolvedType = "" if ($p.type) { if ($p.type -is [array] -or $p.type -is [System.Collections.IList]) { $resolvedType = @($p.type | ForEach-Object { Resolve-TypeStr "$_" }) } else { $resolvedType = Resolve-TypeStr "$($p.type)" } } $parsed = @{ name = "$($p.name)" type = $resolvedType value = $p.value autoDates = $false } if ($p.expression) { $parsed.expression = "$($p.expression)" } if ($p.availableAsField -eq $false) { $parsed.availableAsField = $false } if ($p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } if ($p.hidden -eq $true) { $parsed.hidden = $true } if ($p.autoDates -eq $true) { $parsed.autoDates = $true } if ($p.nilValue -eq $true) { $parsed.nilValue = $true } } # @autoDates implies use=Always + denyIncompleteValues=true by default # (derived &НачалоПериода/&КонецПериода need a populated period). # Explicit values in object form override these defaults. if ($parsed.autoDates) { $isObj = ($p -isnot [string]) -and ($null -ne $p) if (-not ($isObj -and $null -ne $p.use)) { $parsed.use = 'Always' } if (-not ($isObj -and $null -ne $p.denyIncompleteValues)) { $parsed.denyIncompleteValues = $true } } Emit-SingleParam -p $p -parsed $parsed # Track parameter for auto dataParameters $script:allParams += @{ name = $parsed.name; hidden = [bool]$parsed.hidden; type = "$($parsed.type)"; value = $parsed.value } # @autoDates: auto-generate НачалоПериода and КонецПериода (canonical БСП pattern). # type=dateTime + DateFractions=DateTime — иначе КонецПериода обрезается до 00:00:00 # и запрос `Дата МЕЖДУ &НачалоПериода И &КонецПериода` теряет данные за последний день. if ($parsed.autoDates) { $paramName = $parsed.name $beginParsed = @{ name = "НачалоПериода"; title = "Начало периода" type = "dateTime"; value = "0001-01-01T00:00:00" useRestriction = $true expression = "&$paramName.ДатаНачала" } Emit-SingleParam -p $null -parsed $beginParsed $endParsed = @{ name = "КонецПериода"; title = "Конец периода" type = "dateTime"; value = "0001-01-01T00:00:00" useRestriction = $true expression = "&$paramName.ДатаОкончания" } Emit-SingleParam -p $null -parsed $endParsed } } } function Test-EmptyValue { param($v) if ($null -eq $v) { return $true } $s = "$v".Trim() if ($s -eq "") { return $true } if ($s -eq "_") { return $true } if ($s.ToLowerInvariant() -eq "null") { return $true } return $false } function Emit-EmptyValue { param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) if ($valueListAllowed) { return } $t = if ($null -eq $type) { "" } else { "$type" } # Нормализация: убираем префикс xs: (валидный для valueType из decompile/DSL) $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } $pf = $tagPrefix if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" } elseif ($t -eq "StandardPeriod") { X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" X "$indent`tCustom" X "$indent`t0001-01-01T00:00:00" X "$indent`t0001-01-01T00:00:00" X "$indent" } elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" } elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00" } elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0" } elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false" } else { # Ref types or unknown — safe nil X "$indent<${pf}value xsi:nil=`"true`"/>" } } function Emit-ParamValue { param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false) if (Test-EmptyValue $val) { Emit-EmptyValue -type $type -indent $indent -tagPrefix "" -valueListAllowed $valueListAllowed return } # val может быть строкой (variant only) или объектом {variant, startDate?, endDate?}. $valIsDict = ($val -is [hashtable]) -or ($val -is [System.Collections.IDictionary]) -or ($val -is [PSCustomObject]) $variantStr = $null $sdStr = $null $edStr = $null if ($valIsDict) { if ($val -is [PSCustomObject]) { if ($val.PSObject.Properties['variant']) { $variantStr = "$($val.variant)" } if ($val.PSObject.Properties['startDate']) { $sdStr = "$($val.startDate)" } if ($val.PSObject.Properties['endDate']) { $edStr = "$($val.endDate)" } } else { if ($val.Contains('variant')) { $variantStr = "$($val['variant'])" } if ($val.Contains('startDate')) { $sdStr = "$($val['startDate'])" } if ($val.Contains('endDate')) { $edStr = "$($val['endDate'])" } } } $valStr = if ($variantStr) { $variantStr } else { "$val" } if ($type -eq "StandardPeriod") { # Platform-pattern: startDate/endDate эмитятся ТОЛЬКО для variant=Custom. # Для всех остальных вариантов (ThisMonth, LastYear, Today, ...) — без дат. X "$indent" X "$indent`t$(Esc-Xml $valStr)" if ($valStr -eq 'Custom') { $sdOut = if ($sdStr) { $sdStr } else { '0001-01-01T00:00:00' } $edOut = if ($edStr) { $edStr } else { '0001-01-01T00:00:00' } X "$indent`t$(Esc-Xml $sdOut)" X "$indent`t$(Esc-Xml $edOut)" } X "$indent" } elseif ($type -match '^date') { X "$indent$(Esc-Xml $valStr)" } elseif ($type -eq "boolean") { X "$indent$(Esc-Xml $valStr)" } elseif ($type -match '^decimal') { X "$indent$(Esc-Xml $valStr)" } elseif ($type -match '^string') { X "$indent$(Esc-Xml $valStr)" } elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent$(Esc-Xml $valStr)" } else { # Guess from value if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent$(Esc-Xml $valStr)" } elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent$(Esc-Xml $valStr)" } elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent$(Esc-Xml $valStr)" } else { X "$indent$(Esc-Xml $valStr)" } } } # === AreaTemplate DSL === # Built-in style presets $script:areaStylePresets = @{ 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 } } # Load user presets from skd-styles.json # Search order (first found wins): 1) definition dir, 2) cwd, 3) scan-up from OutputPath for presets/skills/skd/ $script:userStylesLoaded = $false $searchPaths = @( (Join-Path $script:queryBaseDir "skd-styles.json"), (Join-Path (Get-Location).Path "skd-styles.json") ) $outResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location).Path $OutputPath } $scanDir = [System.IO.Path]::GetDirectoryName($outResolved) while ($scanDir) { $searchPaths += Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "skd") "skd-styles.json" $parentDir = Split-Path $scanDir -Parent if ($parentDir -eq $scanDir) { break } $scanDir = $parentDir } foreach ($stylesFile in $searchPaths) { if (Test-Path $stylesFile) { $userStyles = Get-Content -Raw -Encoding UTF8 $stylesFile | ConvertFrom-Json foreach ($prop in $userStyles.PSObject.Properties) { $preset = @{} # Start from 'data' defaults foreach ($k in $script:areaStylePresets['data'].Keys) { $preset[$k] = $script:areaStylePresets['data'][$k] } # If overriding existing preset, start from it instead if ($script:areaStylePresets.ContainsKey($prop.Name)) { foreach ($k in $script:areaStylePresets[$prop.Name].Keys) { $preset[$k] = $script:areaStylePresets[$prop.Name][$k] } } # Apply user overrides foreach ($up in $prop.Value.PSObject.Properties) { $preset[$up.Name] = $up.Value } $script:areaStylePresets[$prop.Name] = $preset } $script:userStylesLoaded = $true break } } function Emit-ColorValue { param([string]$color, [string]$indent) # Префиксы style:/web:/win: → соответствующий xmlns + dN:Name $colorPrefixToUri = @{ 'style:' = 'http://v8.1c.ru/8.1/data/ui/style' 'web:' = 'http://v8.1c.ru/8.1/data/ui/colors/web' 'win:' = 'http://v8.1c.ru/8.1/data/ui/colors/windows' } foreach ($pfx in $colorPrefixToUri.Keys) { if ($color.StartsWith($pfx)) { $name = $color.Substring($pfx.Length) $uri = $colorPrefixToUri[$pfx] X "$indentd8p1:$name" return } } X "$indent$(Esc-Xml $color)" } function Emit-CellAppearance { param($style, [double]$width = 0, [bool]$vMerge = $false, [bool]$hMerge = $false, [double]$minHeight = 0, $extraItems = @()) $ind = "`t`t`t`t`t`t" # Если ничего внутри appearance не будет — не эмитим блок вовсе # (оригинал платформы для cells без атрибутов не пишет ). $hasContent = $style.bgColor -or $style.textColor -or $style.borders -or $style.font -or ` $style.hAlign -or $style.vAlign -or $style.wrap -or ` ($width -gt 0) -or ($minHeight -gt 0) -or $vMerge -or $hMerge -or ` ($extraItems -and @($extraItems).Count -gt 0) if (-not $hasContent) { return } X "`t`t`t`t`t" # Background color if ($style.bgColor) { X "$ind" X "$ind`tЦветФона" Emit-ColorValue $style.bgColor "$ind`t" X "$ind" } # Text color if ($style.textColor) { X "$ind" X "$ind`tЦветТекста" Emit-ColorValue $style.textColor "$ind`t" X "$ind" } # Border color + border style (4 sides) if ($style.borders) { if ($style.borderColor) { X "$ind" X "$ind`tЦветГраницы" Emit-ColorValue $style.borderColor "$ind`t" X "$ind" } X "$ind" X "$ind`tСтильГраницы" X "$ind`t" X "$ind`t`tNone" X "$ind`t" foreach ($side in @('Слева','Сверху','Справа','Снизу')) { X "$ind`t" X "$ind`t`tСтильГраницы.$side" X "$ind`t`t" X "$ind`t`t`tSolid" X "$ind`t`t" X "$ind`t" } X "$ind" } # Font (skip if style has no font configured — for "none" preset) if ($style.font) { $boldStr = if ($style.bold) { "true" } else { "false" } $italicStr = if ($style.italic) { "true" } else { "false" } X "$ind" X "$ind`tШрифт" X "$ind`t" X "$ind" } # Horizontal alignment if ($style.hAlign) { X "$ind" X "$ind`tГоризонтальноеПоложение" X "$ind`t$(Esc-Xml $style.hAlign)" X "$ind" } # Vertical alignment if ($style.vAlign) { X "$ind" X "$ind`tВертикальноеПоложение" X "$ind`t$(Esc-Xml $style.vAlign)" X "$ind" } # Text placement (wrap) if ($style.wrap) { X "$ind" X "$ind`tРазмещение" X "$ind`tWrap" X "$ind" } # Width if ($width -gt 0) { X "$ind" X "$ind`tМинимальнаяШирина" X "$ind`t$width" X "$ind" X "$ind" X "$ind`tМаксимальнаяШирина" X "$ind`t$width" X "$ind" } # Min height if ($minHeight -gt 0) { X "$ind" X "$ind`tМинимальнаяВысота" X "$ind`t$minHeight" X "$ind" } # Vertical merge if ($vMerge) { X "$ind" X "$ind`tОбъединятьПоВертикали" X "$ind`ttrue" X "$ind" } # Horizontal merge if ($hMerge) { X "$ind" X "$ind`tОбъединятьПоГоризонтали" X "$ind`ttrue" X "$ind" } # Extra appearance items (e.g. drilldown Расшифровка) foreach ($ei in $extraItems) { X $ei } X "`t`t`t`t`t" } # Cell может быть string ("text"/"{param}"/"|"/">"/null) или объектом {value, style}. # Helpers извлекают значение и эффективный стиль ячейки. function Get-CellValue { param($cell) if ($null -eq $cell) { return $null } if ($cell -is [string]) { return $cell } if ($cell -is [hashtable] -or $cell -is [System.Collections.IDictionary]) { if ($cell.Contains('value')) { return $cell['value'] } return $cell # multilang dict без обёртки } if ($cell.PSObject -and $cell.PSObject.Properties['value']) { return $cell.value } # PSCustomObject без 'value' — это multilang dict ({ru, en, ...}), отдаём как есть if ($cell -is [PSCustomObject]) { return $cell } return $null } function Get-CellStyleOrDefault { param($cell, $defaultStyle) if ($null -ne $cell -and -not ($cell -is [string]) -and $cell.PSObject -and $cell.PSObject.Properties['style']) { $sName = "$($cell.style)" if ($script:areaStylePresets.ContainsKey($sName)) { return $script:areaStylePresets[$sName] } Write-Warning "Unknown cell style preset '$sName', falling back to template default" } return $defaultStyle } function Emit-AreaTemplateDSL { param($t) $styleName = if ($t.style) { "$($t.style)" } else { "data" } if (-not $script:areaStylePresets.ContainsKey($styleName)) { Write-Warning "Unknown area style preset '$styleName', falling back to 'data'" $styleName = "data" } $style = $script:areaStylePresets[$styleName] $rows = @($t.rows) # PS-quirk: if-expression unwraps single-element @() результат # (`$x = if (...) { @($arr) }` даёт скаляр при одном элементе). # Используем обычный if вместо if-expression. $widths = @() if ($t.widths) { $widths = @($t.widths) } $minHeight = if ($t.minHeight) { [double]$t.minHeight } else { 0 } $colCount = if ($widths.Count -gt 0) { $widths.Count } else { $rows[0].Count } # Build vertical merge map: vMerge[row][col] = $true if cell is merged with above $vMerge = @{} for ($r = $rows.Count - 1; $r -ge 1; $r--) { $vMerge[$r] = @{} for ($c = 0; $c -lt $colCount; $c++) { $cellValStr = Get-CellValue $rows[$r][$c] if ($cellValStr -eq '|') { $vMerge[$r][$c] = $true } } } if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} } # Build horizontal merge map: hMerge[row][col] = $true if cell is merged with left $hMerge = @{} for ($r = 0; $r -lt $rows.Count; $r++) { $hMerge[$r] = @{} for ($c = 0; $c -lt $colCount; $c++) { $cellValStr = Get-CellValue $rows[$r][$c] if ($cellValStr -eq '>') { $hMerge[$r][$c] = $true } } } # Build drilldown map: param_name -> drilldown_value (только для shortcut-формы — drilldown:string). # Форма C (drilldown:object) — DetailsAreaTemplateParameter с произвольным именем, в map не идёт. $drilldownMap = @{} if ($t.parameters) { foreach ($tp in $t.parameters) { if ($tp.drilldown -and ($tp.drilldown -is [string])) { $drilldownMap["$($tp.name)"] = "$($tp.drilldown)" } } } X "`t" } # Эмиссия одного параметра шаблона. Различает три формы: # A. { name, expression } → ExpressionAreaTemplateParameter # B. { name, expression, drilldown: "X" } → Expression + Details(Расшифровка_X, ИмяРесурса, DrillDown) [shortcut] # C. { name, drilldown: { field, expression, action? } } → DetailsAreaTemplateParameter с произвольным name function Emit-AreaTemplateParameter { param($tp, [string]$indent) # Определяем форму C: drilldown — объект с полем field или expression. $dd = $tp.drilldown $ddIsObject = $false if ($null -ne $dd) { if ($dd -is [hashtable] -or $dd -is [System.Collections.IDictionary]) { $ddIsObject = $true } elseif ($dd -is [PSCustomObject]) { $ddIsObject = $true } } if ($ddIsObject) { # Форма C $ddField = if ($dd -is [PSCustomObject]) { "$($dd.field)" } else { "$($dd['field'])" } $ddExpr = if ($dd -is [PSCustomObject]) { "$($dd.expression)" } else { "$($dd['expression'])" } $ddActV = $null if ($dd -is [PSCustomObject] -and $dd.PSObject.Properties['action']) { $ddActV = "$($dd.action)" } elseif (($dd -is [hashtable] -or $dd -is [System.Collections.IDictionary]) -and $dd.Contains('action')) { $ddActV = "$($dd['action'])" } $ddAct = if ($ddActV) { $ddActV } else { 'DrillDown' } X "$indent" X "$indent`t$(Esc-Xml "$($tp.name)")" X "$indent`t" X "$indent`t`t$(Esc-Xml $ddField)" X "$indent`t`t$(Esc-Xml $ddExpr)" X "$indent`t" X "$indent`t$(Esc-Xml $ddAct)" X "$indent" return } # Форма A или B X "$indent" X "$indent`t$(Esc-Xml "$($tp.name)")" X "$indent`t$(Esc-Xml "$($tp.expression)")" X "$indent" if ($dd -and ($dd -is [string])) { # Форма B: shortcut Расшифровка_ + ИмяРесурса + DrillDown $ddVal = "$dd" X "$indent" X "$indent`tРасшифровка_$(Esc-Xml $ddVal)" X "$indent`t" X "$indent`t`tИмяРесурса" X "$indent`t`t`"$(Esc-Xml $ddVal)`"" X "$indent`t" X "$indent`tDrillDown" X "$indent" } } # === Templates === function Emit-Templates { if (-not $def.templates) { return } foreach ($t in $def.templates) { if ($t.rows) { # Compact DSL mode Emit-AreaTemplateDSL $t } else { # Raw XML mode X "`t" } } } # === FieldTemplates === # Привязка