Files
cc-1c-skills/.claude/skills/skd-compile/scripts/skd-compile.ps1
T
Nick Shirokov 82e70d2c30 feat(skd-compile,form-compile): Phase 3 — project-level presets
- skd-compile v1.12: scan-up from OutputPath for presets/skills/skd/skd-styles.json (PS1+PY)
- form-compile presets/README.md: full preset documentation (sections, keys, enums, example)
- docs/form-guide.md: --from-object mode + project-level presets sections
- skd-compile SKILL.md: updated styles search path description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:08:22 +03:00

2183 lines
74 KiB
PowerShell

# skd-compile v1.12 — 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
# --- 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
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('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
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, [string]$text, [string]$indent)
X "$indent<$tag xsi:type=`"v8:LocalStringType`">"
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
X "$indent`t</v8:item>"
X "$indent</$tag>"
}
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["стандартныйпериод"] = "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([string]$typeStr, [string]$indent)
if (-not $typeStr) { return }
# Resolve synonyms first
$typeStr = Resolve-TypeStr $typeStr
# boolean
if ($typeStr -eq "boolean") {
X "$indent<v8:Type>xs:boolean</v8:Type>"
return
}
# string or string(N)
if ($typeStr -match '^string(\((\d+)\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "0" }
X "$indent<v8:Type>xs:string</v8:Type>"
X "$indent<v8:StringQualifiers>"
X "$indent`t<v8:Length>$len</v8:Length>"
X "$indent`t<v8:AllowedLength>Variable</v8:AllowedLength>"
X "$indent</v8:StringQualifiers>"
return
}
# decimal(D,F) or decimal(D,F,nonneg)
if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') {
$digits = $Matches[1]
$fraction = $Matches[2]
$sign = if ($Matches[3]) { "Nonnegative" } else { "Any" }
X "$indent<v8:Type>xs:decimal</v8:Type>"
X "$indent<v8:NumberQualifiers>"
X "$indent`t<v8:Digits>$digits</v8:Digits>"
X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>"
X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>"
X "$indent</v8:NumberQualifiers>"
return
}
# date / dateTime
if ($typeStr -match '^(date|dateTime)$') {
$fractions = switch ($typeStr) {
"date" { "Date" }
"dateTime" { "DateTime" }
}
X "$indent<v8:Type>xs:dateTime</v8:Type>"
X "$indent<v8:DateQualifiers>"
X "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>"
X "$indent</v8:DateQualifiers>"
return
}
# StandardPeriod
if ($typeStr -eq "StandardPeriod") {
X "$indent<v8:Type>v8:StandardPeriod</v8:Type>"
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 "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>"
return
}
# Fallback — assume dot-qualified types are also config references
if ($typeStr.Contains('.')) {
X "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>"
return
}
X "$indent<v8:Type>$(Esc-Xml $typeStr)</v8:Type>"
}
# --- 5. Field shorthand parser ---
function Parse-FieldShorthand {
param([string]$s)
$result = @{
dataPath = ""; field = ""; title = ""; type = ""
roles = @(); restrict = @(); appearance = @{}
}
# 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+', '')
# 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
}
# --- 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 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"
if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') {
$result.name = $Matches[1].Trim()
$result.type = Resolve-TypeStr ($Matches[2].Trim())
if ($Matches[4]) {
$result.value = $Matches[4].Trim()
}
} else {
$result.name = $s.Trim()
}
return $result
}
# --- 8. Calculated field shorthand parser ---
function Parse-CalcShorthand {
param([string]$s)
# "DataPath = Expression"
$idx = $s.IndexOf('=')
if ($idx -gt 0) {
return @{
dataPath = $s.Substring(0, $idx).Trim()
expression = $s.Substring($idx + 1).Trim()
}
}
return @{ dataPath = $s.Trim(); expression = "" }
}
# --- 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"
}
# --- 11. Emit sections ---
# === DataSources ===
function Emit-DataSources {
foreach ($ds in $dataSources) {
X "`t<dataSource>"
X "`t`t<name>$(Esc-Xml $ds.name)</name>"
X "`t`t<dataSourceType>$(Esc-Xml $ds.type)</dataSourceType>"
X "`t</dataSource>"
}
}
# === Fields ===
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) { Resolve-TypeStr "$($fieldDef.type)" } else { "" }
roles = @()
restrict = @()
appearance = @{}
}
# Parse role
if ($fieldDef.role) {
if ($fieldDef.role -is [string]) {
$f.roles = @($fieldDef.role)
} else {
# Object form — collect truthy keys
$roleObj = $fieldDef.role
foreach ($prop in $roleObj.PSObject.Properties) {
if ($prop.Value -eq $true) { $f.roles += $prop.Name }
}
}
}
# Parse restrictions
if ($fieldDef.restrict) {
$f.restrict = @($fieldDef.restrict)
}
# Parse appearance
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)
}
# role object extras
if ($fieldDef.role -and $fieldDef.role -isnot [string]) {
$f["roleObj"] = $fieldDef.role
}
}
X "$indent<field xsi:type=`"DataSetFieldField`">"
X "$indent`t<dataPath>$(Esc-Xml $f.dataPath)</dataPath>"
X "$indent`t<field>$(Esc-Xml $f.field)</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<useRestriction>"
foreach ($r in $f.restrict) {
$xmlName = $restrictMap["$r"]
if ($xmlName) {
X "$indent`t`t<$xmlName>true</$xmlName>"
}
}
X "$indent`t</useRestriction>"
}
# AttributeUseRestriction
if ($f["attrRestrict"] -and $f["attrRestrict"].Count -gt 0) {
X "$indent`t<attributeUseRestriction>"
foreach ($r in $f["attrRestrict"]) {
$xmlName = $restrictMap["$r"]
if ($xmlName) {
X "$indent`t`t<$xmlName>true</$xmlName>"
}
}
X "$indent`t</attributeUseRestriction>"
}
# Role
if ($f.roles.Count -gt 0 -or $f["roleObj"]) {
X "$indent`t<role>"
foreach ($role in $f.roles) {
if ($role -eq "period") {
# @period -> periodNumber + periodType (not <dcscom:period>)
X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>"
X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>"
} else {
X "$indent`t`t<dcscom:$role>true</dcscom:$role>"
}
}
if ($f["roleObj"]) {
$ro = $f["roleObj"]
if ($ro.accountTypeExpression) {
X "$indent`t`t<dcscom:accountTypeExpression>$(Esc-Xml "$($ro.accountTypeExpression)")</dcscom:accountTypeExpression>"
}
if ($ro.balanceGroup) {
X "$indent`t`t<dcscom:balanceGroup>$(Esc-Xml "$($ro.balanceGroup)")</dcscom:balanceGroup>"
}
}
X "$indent`t</role>"
}
# ValueType
if ($f.type) {
X "$indent`t<valueType>"
Emit-ValueType -typeStr $f.type -indent "$indent`t`t"
X "$indent`t</valueType>"
}
# Appearance
if ($f.appearance -and $f.appearance.Count -gt 0) {
X "$indent`t<appearance>"
foreach ($key in $f.appearance.Keys) {
$val = $f.appearance[$key]
X "$indent`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
X "$indent`t`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
if ($key -eq "ГоризонтальноеПоложение") {
X "$indent`t`t`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml $val)</dcscor:value>"
} else {
X "$indent`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $val)</dcscor:value>"
}
X "$indent`t`t</dcscor:item>"
}
X "$indent`t</appearance>"
}
# PresentationExpression
if ($f["presentationExpression"]) {
X "$indent`t<presentationExpression>$(Esc-Xml $f["presentationExpression"])</presentationExpression>"
}
X "$indent</field>"
}
# === DataSets ===
function Emit-DataSet {
param($ds, [string]$indent)
# Determine type
if ($ds.items) {
$dsType = "DataSetUnion"
} elseif ($ds.objectName) {
$dsType = "DataSetObject"
} else {
$dsType = "DataSetQuery"
}
X "$indent<dataSet xsi:type=`"$dsType`">"
X "$indent`t<name>$(Esc-Xml "$($ds.name)")</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<dataSource>$(Esc-Xml $src)</dataSource>"
}
# Type-specific content
if ($dsType -eq "DataSetQuery") {
$queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir
X "$indent`t<query>$(Esc-Xml $queryText)</query>"
if ($ds.autoFillFields -eq $false) {
X "$indent`t<autoFillFields>false</autoFillFields>"
}
} elseif ($dsType -eq "DataSetObject") {
X "$indent`t<objectName>$(Esc-Xml "$($ds.objectName)")</objectName>"
} elseif ($dsType -eq "DataSetUnion") {
foreach ($item in $ds.items) {
# Union items are nested dataSets
Emit-DataSet -ds $item -indent "$indent`t" | Out-Null
}
}
X "$indent</dataSet>"
}
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<dataSetLink>"
$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<sourceDataSet>$(Esc-Xml $srcDS)</sourceDataSet>"
X "`t`t<destinationDataSet>$(Esc-Xml $dstDS)</destinationDataSet>"
X "`t`t<sourceExpression>$(Esc-Xml $srcEx)</sourceExpression>"
X "`t`t<destinationExpression>$(Esc-Xml $dstEx)</destinationExpression>"
if ($link.parameter) {
X "`t`t<parameter>$(Esc-Xml "$($link.parameter)")</parameter>"
}
X "`t</dataSetLink>"
}
}
# === CalculatedFields ===
function Emit-CalcFields {
if (-not $def.calculatedFields) { return }
foreach ($cf in $def.calculatedFields) {
if ($cf -is [string]) {
$parsed = Parse-CalcShorthand $cf
} else {
$dp = if ($cf.dataPath) { "$($cf.dataPath)" } else { "$($cf.field)" }
$parsed = @{
dataPath = $dp
expression = "$($cf.expression)"
}
}
X "`t<calculatedField>"
X "`t`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>"
X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>"
if ($cf -isnot [string]) {
if ($cf.title) {
Emit-MLText -tag "title" -text "$($cf.title)" -indent "`t`t"
}
if ($cf.type) {
$cfType = Resolve-TypeStr "$($cf.type)"
X "`t`t<valueType>"
Emit-ValueType -typeStr $cfType -indent "`t`t`t"
X "`t`t</valueType>"
}
$restrictVal = if ($cf.restrict) { $cf.restrict } elseif ($cf.useRestriction) { $cf.useRestriction } else { $null }
if ($restrictVal) {
X "`t`t<useRestriction>"
if ($restrictVal -is [System.Management.Automation.PSCustomObject] -or $restrictVal -is [hashtable]) {
# Object form: { "field": true, "condition": true, ... }
foreach ($prop in $restrictVal.PSObject.Properties) {
if ($prop.Value -eq $true) {
X "`t`t`t<$($prop.Name)>true</$($prop.Name)>"
}
}
} else {
# Array form: ["noField", "noFilter", ...]
$restrictMap = @{
"noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition"
"noGroup" = "group"; "noOrder" = "order"
}
foreach ($r in $restrictVal) {
$xmlName = $restrictMap["$r"]
if ($xmlName) { X "`t`t`t<$xmlName>true</$xmlName>" }
}
}
X "`t`t</useRestriction>"
}
if ($cf.appearance) {
X "`t`t<appearance>"
foreach ($prop in $cf.appearance.PSObject.Properties) {
X "`t`t`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
X "`t`t`t`t<dcscor:parameter>$(Esc-Xml $prop.Name)</dcscor:parameter>"
X "`t`t`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($prop.Value)")</dcscor:value>"
X "`t`t`t</dcscor:item>"
}
X "`t`t</appearance>"
}
}
X "`t</calculatedField>"
}
}
# === 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<totalField>"
X "`t`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>"
X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>"
if ($parsed.groups) {
foreach ($g in $parsed.groups) {
X "`t`t<group>$(Esc-Xml "$g")</group>"
}
}
X "`t</totalField>"
}
}
# === Parameters ===
function Emit-SingleParam {
param($p, $parsed)
X "`t<parameter>"
X "`t`t<name>$(Esc-Xml $parsed.name)</name>"
# Title (from parsed first, then from object form)
$title = ""
if ($parsed.title) {
$title = "$($parsed.title)"
} elseif ($p -isnot [string] -and $p.title) {
$title = "$($p.title)"
}
if ($title) {
Emit-MLText -tag "title" -text $title -indent "`t`t"
}
# ValueType
if ($parsed.type) {
X "`t`t<valueType>"
Emit-ValueType -typeStr $parsed.type -indent "`t`t`t"
X "`t`t</valueType>"
}
# Value
Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t"
# Hidden implies useRestriction=true + availableAsField=false
if ($parsed.hidden -eq $true) {
$parsed.availableAsField = $false
$parsed.useRestriction = $true
}
# UseRestriction
if ($parsed.useRestriction -eq $true -or ($p -isnot [string] -and $p.useRestriction -eq $true)) {
X "`t`t<useRestriction>true</useRestriction>"
}
# Expression
if ($parsed.expression) {
X "`t`t<expression>$(Esc-Xml $parsed.expression)</expression>"
}
# AvailableAsField
if ($parsed.availableAsField -eq $false) {
X "`t`t<availableAsField>false</availableAsField>"
}
# ValueListAllowed
if ($parsed.valueListAllowed -eq $true) {
X "`t`t<valueListAllowed>true</valueListAllowed>"
}
# AvailableValues
if ($p -isnot [string] -and $p.availableValues) {
foreach ($av in $p.availableValues) {
$avVal = "$($av.value)"
$avType = "xs:string"
if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
$avType = "dcscor:DesignTimeValue"
}
X "`t`t<availableValue>"
X "`t`t`t<value xsi:type=`"$avType`">$(Esc-Xml $avVal)</value>"
if ($av.presentation) {
X "`t`t`t<presentation xsi:type=`"v8:LocalStringType`">"
X "`t`t`t`t<v8:item>"
X "`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t<v8:content>$(Esc-Xml "$($av.presentation)")</v8:content>"
X "`t`t`t`t</v8:item>"
X "`t`t`t</presentation>"
}
X "`t`t</availableValue>"
}
}
# DenyIncompleteValues
if ($p -isnot [string] -and $p.denyIncompleteValues -eq $true) {
X "`t`t<denyIncompleteValues>true</denyIncompleteValues>"
}
# Use
if ($p -isnot [string] -and $p.use) {
X "`t`t<use>$(Esc-Xml "$($p.use)")</use>"
}
X "`t</parameter>"
}
$script:allParams = @()
function Emit-Parameters {
if (-not $def.parameters) { return }
foreach ($p in $def.parameters) {
if ($p -is [string]) {
$parsed = Parse-ParamShorthand $p
} else {
$parsed = @{
name = "$($p.name)"
type = if ($p.type) { Resolve-TypeStr "$($p.type)" } else { "" }
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 }
}
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)
if ($parsed.autoDates) {
$paramName = $parsed.name
$beginParsed = @{
name = "ДатаНачала"; title = "Начало периода"
type = "date"; value = "0001-01-01T00:00:00"
useRestriction = $true
expression = "&$paramName.ДатаНачала"
}
Emit-SingleParam -p $null -parsed $beginParsed
$endParsed = @{
name = "ДатаОкончания"; title = "Конец периода"
type = "date"; value = "0001-01-01T00:00:00"
useRestriction = $true
expression = "&$paramName.ДатаОкончания"
}
Emit-SingleParam -p $null -parsed $endParsed
}
}
}
function Emit-ParamValue {
param([string]$type, $val, [string]$indent)
if ($null -eq $val) { return }
$valStr = "$val"
if ($type -eq "StandardPeriod") {
# val is a period variant string like "LastMonth" or "Custom".
# Always emit startDate/endDate to match how 1C Designer saves the schema.
X "$indent<value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>"
X "$indent`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent</value>"
} elseif ($type -match '^date') {
X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
} elseif ($type -eq "boolean") {
X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>"
} elseif ($type -match '^decimal') {
X "$indent<value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</value>"
} elseif ($type -match '^string') {
X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>"
} else {
# Guess from value
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') {
X "$indent<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
} elseif ($valStr -eq "true" -or $valStr -eq "false") {
X "$indent<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>"
} elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>"
} else {
X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>"
}
}
}
# === AreaTemplate DSL ===
# Built-in style presets
$script:areaStylePresets = @{
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)
if ($color.StartsWith('style:')) {
$styleName = $color.Substring(6)
X "$indent<dcscor:value xmlns:d8p1=`"http://v8.1c.ru/8.1/data/ui/style`" xsi:type=`"v8ui:Color`">d8p1:$styleName</dcscor:value>"
} else {
X "$indent<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $color)</dcscor:value>"
}
}
function Emit-CellAppearance {
param($style, [double]$width = 0, [bool]$vMerge = $false, [bool]$hMerge = $false, [double]$minHeight = 0, $extraItems = @())
$ind = "`t`t`t`t`t"
X "`t`t`t`t<dcsat:appearance>"
# Background color
if ($style.bgColor) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ЦветФона</dcscor:parameter>"
Emit-ColorValue $style.bgColor "$ind`t"
X "$ind</dcscor:item>"
}
# Text color
if ($style.textColor) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ЦветТекста</dcscor:parameter>"
Emit-ColorValue $style.textColor "$ind`t"
X "$ind</dcscor:item>"
}
# Border color + border style (4 sides)
if ($style.borders) {
if ($style.borderColor) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ЦветГраницы</dcscor:parameter>"
Emit-ColorValue $style.borderColor "$ind`t"
X "$ind</dcscor:item>"
}
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>СтильГраницы</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"0`" gap=`"false`">"
X "$ind`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">None</v8ui:style>"
X "$ind`t</dcscor:value>"
foreach ($side in @('Слева','Сверху','Справа','Снизу')) {
X "$ind`t<dcscor:item>"
X "$ind`t`t<dcscor:parameter>СтильГраницы.$side</dcscor:parameter>"
X "$ind`t`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"1`" gap=`"false`">"
X "$ind`t`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
X "$ind`t`t</dcscor:value>"
X "$ind`t</dcscor:item>"
}
X "$ind</dcscor:item>"
}
# Font
$boldStr = if ($style.bold) { "true" } else { "false" }
$italicStr = if ($style.italic) { "true" } else { "false" }
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>Шрифт</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"v8ui:Font`" faceName=`"$($style.font)`" height=`"$($style.fontSize)`" bold=`"$boldStr`" italic=`"$italicStr`" underline=`"false`" strikeout=`"false`" kind=`"Absolute`" scale=`"100`"/>"
X "$ind</dcscor:item>"
# Horizontal alignment
if ($style.hAlign) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ГоризонтальноеПоложение</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"v8ui:HorizontalAlign`">$(Esc-Xml $style.hAlign)</dcscor:value>"
X "$ind</dcscor:item>"
}
# Vertical alignment
if ($style.vAlign) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ВертикальноеПоложение</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"v8ui:VerticalAlign`">$(Esc-Xml $style.vAlign)</dcscor:value>"
X "$ind</dcscor:item>"
}
# Text placement (wrap)
if ($style.wrap) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>Размещение</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"dcscor:DataCompositionTextPlacementType`">Wrap</dcscor:value>"
X "$ind</dcscor:item>"
}
# Width
if ($width -gt 0) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>МинимальнаяШирина</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$width</dcscor:value>"
X "$ind</dcscor:item>"
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>МаксимальнаяШирина</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$width</dcscor:value>"
X "$ind</dcscor:item>"
}
# Min height
if ($minHeight -gt 0) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>МинимальнаяВысота</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"xs:decimal`">$minHeight</dcscor:value>"
X "$ind</dcscor:item>"
}
# Vertical merge
if ($vMerge) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ОбъединятьПоВертикали</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>"
X "$ind</dcscor:item>"
}
# Horizontal merge
if ($hMerge) {
X "$ind<dcscor:item>"
X "$ind`t<dcscor:parameter>ОбъединятьПоГоризонтали</dcscor:parameter>"
X "$ind`t<dcscor:value xsi:type=`"xs:boolean`">true</dcscor:value>"
X "$ind</dcscor:item>"
}
# Extra appearance items (e.g. drilldown Расшифровка)
foreach ($ei in $extraItems) { X $ei }
X "`t`t`t`t</dcsat:appearance>"
}
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)
$widths = if ($t.widths) { @($t.widths) } else { @() }
$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++) {
$cellVal = $rows[$r][$c]
if ($cellVal -is [string] -and $cellVal -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++) {
$cellVal = $rows[$r][$c]
if ($cellVal -is [string] -and $cellVal -eq '>') {
$hMerge[$r][$c] = $true
}
}
}
# Build drilldown map: param_name -> drilldown_value
$drilldownMap = @{}
if ($t.parameters) {
foreach ($tp in $t.parameters) {
if ($tp.drilldown) { $drilldownMap["$($tp.name)"] = "$($tp.drilldown)" }
}
}
X "`t<template>"
X "`t`t<name>$(Esc-Xml "$($t.name)")</name>"
X "`t`t<template xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:AreaTemplate`">"
for ($r = 0; $r -lt $rows.Count; $r++) {
X "`t`t`t<dcsat:item xsi:type=`"dcsat:TableRow`">"
for ($c = 0; $c -lt $colCount; $c++) {
$cellVal = $rows[$r][$c]
$w = if ($c -lt $widths.Count) { [double]$widths[$c] } else { 0 }
$isVMerged = $vMerge[$r][$c] -eq $true
$isHMerged = $hMerge[$r][$c] -eq $true
X "`t`t`t`t<dcsat:tableCell>"
if ($isVMerged) {
# Vertically merged cell — only appearance with vMerge flag + width
Emit-CellAppearance $style $w $true
} elseif ($isHMerged) {
# Horizontally merged cell — only appearance with hMerge flag + width
Emit-CellAppearance $style $w $false $true
} else {
# Cell value
if ($null -ne $cellVal -and $cellVal -ne '') {
$cellStr = "$cellVal"
# Unescape \| and \>
if ($cellStr -eq '\|') { $cellStr = '|' }
elseif ($cellStr -eq '\>') { $cellStr = '>' }
if ($cellStr -match '^\{(.+)\}$') {
# Parameter reference
$paramName = $Matches[1]
X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">"
X "`t`t`t`t`t`t<dcsat:value xsi:type=`"dcscor:Parameter`">$(Esc-Xml $paramName)</dcsat:value>"
X "`t`t`t`t`t</dcsat:item>"
# Build drilldown appearance extra items
$cellExtraItems = @()
if ($drilldownMap.ContainsKey($paramName)) {
$ddVal = $drilldownMap[$paramName]
$cellExtraItems += "`t`t`t`t`t<dcscor:item>"
$cellExtraItems += "`t`t`t`t`t`t<dcscor:parameter>Расшифровка</dcscor:parameter>"
$cellExtraItems += "`t`t`t`t`t`t<dcscor:value xsi:type=`"dcscor:Parameter`">Расшифровка_$ddVal</dcscor:value>"
$cellExtraItems += "`t`t`t`t`t</dcscor:item>"
}
} else {
# Static text
X "`t`t`t`t`t<dcsat:item xsi:type=`"dcsat:Field`">"
X "`t`t`t`t`t`t<dcsat:value xsi:type=`"v8:LocalStringType`">"
X "`t`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellStr)</v8:content>"
X "`t`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t`t</dcsat:value>"
X "`t`t`t`t`t</dcsat:item>"
}
}
# Appearance
$h = if ($r -eq 0) { $minHeight } else { 0 }
if (-not $cellExtraItems) { $cellExtraItems = @() }
Emit-CellAppearance $style $w $false $false $h $cellExtraItems
$cellExtraItems = @()
}
X "`t`t`t`t</dcsat:tableCell>"
}
X "`t`t`t</dcsat:item>"
}
X "`t`t</template>"
# Parameters (reuse existing logic)
if ($t.parameters) {
foreach ($tp in $t.parameters) {
X "`t`t<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:ExpressionAreaTemplateParameter`">"
X "`t`t`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>"
X "`t`t`t<dcsat:expression>$(Esc-Xml "$($tp.expression)")</dcsat:expression>"
X "`t`t</parameter>"
# Drilldown parameter
if ($tp.drilldown) {
$ddVal = "$($tp.drilldown)"
X "`t`t<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">"
X "`t`t`t<dcsat:name>Расшифровка_$(Esc-Xml $ddVal)</dcsat:name>"
X "`t`t`t<dcsat:fieldExpression>"
X "`t`t`t`t<dcsat:field>ИмяРесурса</dcsat:field>"
X "`t`t`t`t<dcsat:expression>`"$(Esc-Xml $ddVal)`"</dcsat:expression>"
X "`t`t`t</dcsat:fieldExpression>"
X "`t`t`t<dcsat:mainAction>DrillDown</dcsat:mainAction>"
X "`t`t</parameter>"
}
}
}
X "`t</template>"
}
# === 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<template>"
X "`t`t<name>$(Esc-Xml "$($t.name)")</name>"
if ($t.template) {
X "`t`t$($t.template)"
}
if ($t.parameters) {
foreach ($tp in $t.parameters) {
X "`t`t<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:ExpressionAreaTemplateParameter`">"
X "`t`t`t<dcsat:name>$(Esc-Xml "$($tp.name)")</dcsat:name>"
X "`t`t`t<dcsat:expression>$(Esc-Xml "$($tp.expression)")</dcsat:expression>"
X "`t`t</parameter>"
# Drilldown parameter
if ($tp.drilldown) {
$ddVal = "$($tp.drilldown)"
X "`t`t<parameter xmlns:dcsat=`"http://v8.1c.ru/8.1/data-composition-system/area-template`" xsi:type=`"dcsat:DetailsAreaTemplateParameter`">"
X "`t`t`t<dcsat:name>Расшифровка_$(Esc-Xml $ddVal)</dcsat:name>"
X "`t`t`t<dcsat:fieldExpression>"
X "`t`t`t`t<dcsat:field>ИмяРесурса</dcsat:field>"
X "`t`t`t`t<dcsat:expression>`"$(Esc-Xml $ddVal)`"</dcsat:expression>"
X "`t`t`t</dcsat:fieldExpression>"
X "`t`t`t<dcsat:mainAction>DrillDown</dcsat:mainAction>"
X "`t`t</parameter>"
}
}
}
X "`t</template>"
}
}
}
# === GroupTemplates ===
function Emit-GroupTemplates {
if (-not $def.groupTemplates) { return }
foreach ($gt in $def.groupTemplates) {
$ttype = if ($gt.templateType) { "$($gt.templateType)" } else { "Header" }
$isHeader = ($ttype -eq 'GroupHeader')
$tag = if ($isHeader) { 'groupHeaderTemplate' } else { 'groupTemplate' }
$xmlTType = if ($isHeader) { 'Header' } else { $ttype }
X "`t<$tag>"
if ($gt.groupName) {
X "`t`t<groupName>$(Esc-Xml "$($gt.groupName)")</groupName>"
} elseif ($gt.groupField) {
X "`t`t<groupField>$(Esc-Xml "$($gt.groupField)")</groupField>"
}
X "`t`t<templateType>$(Esc-Xml $xmlTType)</templateType>"
X "`t`t<template>$(Esc-Xml "$($gt.template)")</template>"
X "`t</$tag>"
}
}
# === Settings Variants ===
function Emit-Selection {
param($items, [string]$indent, [switch]$skipAuto)
if (-not $items -or $items.Count -eq 0) { return }
X "$indent<dcsset:selection>"
foreach ($item in $items) {
if ($item -is [string]) {
if ($item -eq "Auto") {
if (-not $skipAuto) {
X "$indent`t<dcsset:item xsi:type=`"dcsset:SelectedItemAuto`"/>"
}
} else {
X "$indent`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml $item)</dcsset:field>"
X "$indent`t</dcsset:item>"
}
} elseif ($item.folder) {
X "$indent`t<dcsset:item xsi:type=`"dcsset:SelectedItemFolder`">"
X "$indent`t`t<dcsset:lwsTitle>"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml "$($item.folder)")</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcsset:lwsTitle>"
foreach ($sub in $item.items) {
$subName = if ($sub -is [string]) { $sub } else { "$($sub.field)" }
X "$indent`t`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
X "$indent`t`t`t<dcsset:field>$(Esc-Xml $subName)</dcsset:field>"
X "$indent`t`t</dcsset:item>"
}
X "$indent`t`t<dcsset:placement>Auto</dcsset:placement>"
X "$indent`t</dcsset:item>"
} else {
X "$indent`t<dcsset:item xsi:type=`"dcsset:SelectedItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>"
if ($item.title) {
X "$indent`t`t<dcsset:lwsTitle>"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml "$($item.title)")</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcsset:lwsTitle>"
}
X "$indent`t</dcsset:item>"
}
}
X "$indent</dcsset:selection>"
}
function Emit-FilterItem {
param($item, [string]$indent)
if ($item.group) {
# FilterItemGroup
$groupType = switch ("$($item.group)") {
"And" { "AndGroup" }
"Or" { "OrGroup" }
"Not" { "NotGroup" }
default { "$($item.group)Group" }
}
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">"
X "$indent`t<dcsset:groupType>$groupType</dcsset:groupType>"
if ($item.items) {
foreach ($sub in $item.items) {
if ($sub -is [string]) {
$parsed = Parse-FilterShorthand $sub
$obj = @{ field = $parsed.field; op = $parsed.op }
if ($parsed.use -eq $false) { $obj.use = $false }
if ($null -ne $parsed.value) { $obj.value = $parsed.value }
if ($parsed["valueType"]) { $obj.valueType = $parsed["valueType"] }
if ($parsed.userSettingID) { $obj.userSettingID = $parsed.userSettingID }
if ($parsed.viewMode) { $obj.viewMode = $parsed.viewMode }
$sub = [pscustomobject]$obj
}
Emit-FilterItem -item $sub -indent "$indent`t"
}
}
X "$indent</dcsset:item>"
return
}
# FilterItemComparison
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
if ($item.use -eq $false) {
X "$indent`t<dcsset:use>false</dcsset:use>"
}
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>"
$compType = $script:comparisonTypes["$($item.op)"]
if (-not $compType) { $compType = "$($item.op)" }
X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>"
# Right value
if ($null -ne $item.value) {
$vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
if (-not $vt) {
$v = $item.value
if ($v -is [bool]) {
$vt = "xs:boolean"
} elseif ($v -is [int] -or $v -is [long] -or $v -is [double]) {
$vt = "xs:decimal"
} elseif ("$v" -match '^\d{4}-\d{2}-\d{2}T') {
$vt = "xs:dateTime"
} else {
$vt = "xs:string"
}
}
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
X "$indent`t<dcsset:right xsi:type=`"$vt`">$vStr</dcsset:right>"
}
if ($item.presentation) {
X "$indent`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml "$($item.presentation)")</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</dcsset:presentation>"
}
if ($item.viewMode) {
X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>"
}
if ($item.userSettingID) {
$uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
}
if ($item.userSettingPresentation) {
X "$indent`t<dcsset:userSettingPresentation xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml "$($item.userSettingPresentation)")</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</dcsset:userSettingPresentation>"
}
X "$indent</dcsset:item>"
}
function Emit-Filter {
param($items, [string]$indent)
if (-not $items -or $items.Count -eq 0) { return }
X "$indent<dcsset:filter>"
foreach ($item in $items) {
if ($item -is [string]) {
# Parse shorthand: "Организация = _ @off @user"
$parsed = Parse-FilterShorthand $item
$filterObj = New-Object PSObject
$filterObj | Add-Member -NotePropertyName "field" -NotePropertyValue $parsed.field
$filterObj | Add-Member -NotePropertyName "op" -NotePropertyValue $parsed.op
if ($parsed.use -eq $false) {
$filterObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false
}
if ($null -ne $parsed.value) {
$filterObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value
}
if ($parsed["valueType"]) {
$filterObj | Add-Member -NotePropertyName "valueType" -NotePropertyValue $parsed["valueType"]
}
if ($parsed.userSettingID) {
$filterObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID
}
if ($parsed.viewMode) {
$filterObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode
}
Emit-FilterItem -item $filterObj -indent "$indent`t"
} else {
Emit-FilterItem -item $item -indent "$indent`t"
}
}
X "$indent</dcsset:filter>"
}
function Emit-Order {
param($items, [string]$indent, [switch]$skipAuto)
if (-not $items -or $items.Count -eq 0) { return }
X "$indent<dcsset:order>"
foreach ($item in $items) {
if ($item -is [string]) {
if ($item -eq "Auto") {
if (-not $skipAuto) {
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>"
}
} else {
$parts = $item -split '\s+'
$field = $parts[0]
$dir = "Asc"
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)desc$') { $dir = "Desc" }
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)asc$') { $dir = "Asc" }
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
X "$indent`t</dcsset:item>"
}
}
}
X "$indent</dcsset:order>"
}
function Emit-AppearanceValue {
param([string]$key, $val, [string]$indent)
X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
if ($val -is [PSCustomObject] -and $val.use -ne $null -and $val.use -eq $false) {
X "$indent`t<dcscor:use>false</dcscor:use>"
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
$actualVal = "$($val.value)"
} else {
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
$actualVal = "$val"
}
# Auto-detect value type
if ($actualVal -match '^(style|web|win):') {
X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>"
} elseif ($actualVal -eq "true" -or $actualVal -eq "false") {
X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>"
} elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
X "$indent`t<dcscor:value xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t<v8:item>"
X "$indent`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t<v8:content>$(Esc-Xml $actualVal)</v8:content>"
X "$indent`t`t</v8:item>"
X "$indent`t</dcscor:value>"
} else {
X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>"
}
X "$indent</dcscor:item>"
}
function Emit-ConditionalAppearance {
param($items, [string]$indent)
if (-not $items -or $items.Count -eq 0) { return }
X "$indent<dcsset:conditionalAppearance>"
foreach ($ca in $items) {
X "$indent`t<dcsset:item>"
# Selection (which fields to apply to; empty = all)
if ($ca.selection -and $ca.selection.Count -gt 0) {
X "$indent`t`t<dcsset:selection>"
foreach ($sel in $ca.selection) {
X "$indent`t`t`t<dcsset:item>"
X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>"
X "$indent`t`t`t</dcsset:item>"
}
X "$indent`t`t</dcsset:selection>"
} else {
X "$indent`t`t<dcsset:selection/>"
}
# Filter (reuse existing Emit-Filter logic)
if ($ca.filter) {
Emit-Filter -items $ca.filter -indent "$indent`t`t"
}
# Appearance (parameter-value pairs)
if ($ca.appearance) {
X "$indent`t`t<dcsset:appearance>"
foreach ($prop in $ca.appearance.PSObject.Properties) {
Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t"
}
X "$indent`t`t</dcsset:appearance>"
}
# Presentation
if ($ca.presentation) {
X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>"
}
# ViewMode
if ($ca.viewMode) {
X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>"
}
# UserSettingID
if ($ca.userSettingID) {
$uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
}
X "$indent`t</dcsset:item>"
}
X "$indent</dcsset:conditionalAppearance>"
}
function Emit-OutputParameters {
param($params, [string]$indent)
if (-not $params) { return }
X "$indent<dcsset:outputParameters>"
foreach ($prop in $params.PSObject.Properties) {
$key = $prop.Name
$val = "$($prop.Value)"
$ptype = $script:outputParamTypes[$key]
if (-not $ptype) { $ptype = "xs:string" }
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
X "$indent`t`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
if ($ptype -eq "mltext") {
X "$indent`t`t<dcscor:value xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml $val)</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcscor:value>"
} else {
X "$indent`t`t<dcscor:value xsi:type=`"$ptype`">$(Esc-Xml $val)</dcscor:value>"
}
X "$indent`t</dcscor:item>"
}
X "$indent</dcsset:outputParameters>"
}
function Emit-DataParameters {
param($items, [string]$indent)
if (-not $items -or $items.Count -eq 0) { return }
X "$indent<dcsset:dataParameters>"
foreach ($dp in $items) {
# Support string shorthand
if ($dp -is [string]) {
$parsed = Parse-DataParamShorthand $dp
$dpObj = New-Object PSObject
$dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter
if ($null -ne $parsed.value) {
$dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value
}
if ($parsed.use -eq $false) {
$dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false
}
if ($parsed.userSettingID) {
$dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID
}
if ($parsed.viewMode) {
$dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode
}
$dp = $dpObj
}
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
if ($dp.use -eq $false) {
X "$indent`t`t<dcscor:use>false</dcscor:use>"
}
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>"
# Value
if ($null -ne $dp.value) {
if ($dp.value -is [PSCustomObject] -and $dp.value.variant) {
# StandardPeriod (object form from JSON)
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml "$($dp.value.variant)")</v8:variant>"
X "$indent`t`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent`t`t</dcscor:value>"
} elseif ($dp.value -is [hashtable] -and $dp.value.variant) {
# StandardPeriod (hashtable from shorthand parser)
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml "$($dp.value.variant)")</v8:variant>"
X "$indent`t`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
X "$indent`t`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
X "$indent`t`t</dcscor:value>"
} elseif ($dp.value -is [bool]) {
$bv = "$($dp.value)".ToLower()
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml $bv)</dcscor:value>"
} elseif ("$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} else {
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
}
}
if ($dp.viewMode) {
X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>"
}
if ($dp.userSettingID) {
$uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }
X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
}
if ($dp.userSettingPresentation) {
X "$indent`t`t<dcsset:userSettingPresentation xsi:type=`"v8:LocalStringType`">"
X "$indent`t`t`t<v8:item>"
X "$indent`t`t`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t`t`t<v8:content>$(Esc-Xml "$($dp.userSettingPresentation)")</v8:content>"
X "$indent`t`t`t</v8:item>"
X "$indent`t`t</dcsset:userSettingPresentation>"
}
X "$indent`t</dcscor:item>"
}
X "$indent</dcsset:dataParameters>"
}
# === Structure items (recursive) ===
function Emit-GroupItems {
param($groupBy, [string]$indent)
if (-not $groupBy -or $groupBy.Count -eq 0) { return }
X "$indent<dcsset:groupItems>"
foreach ($field in $groupBy) {
if ($field -is [string]) {
X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
X "$indent`t`t<dcsset:groupType>Items</dcsset:groupType>"
X "$indent`t`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>"
X "$indent`t`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>"
X "$indent`t`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>"
X "$indent`t</dcsset:item>"
} else {
# Object form
X "$indent`t<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($field.field)")</dcsset:field>"
$gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" }
X "$indent`t`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>"
$pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" }
X "$indent`t`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>"
X "$indent`t`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>"
X "$indent`t`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>"
X "$indent`t</dcsset:item>"
}
}
X "$indent</dcsset:groupItems>"
}
# Parse structure string shorthand: "Организация > Номенклатура > details"
function Parse-StructureShorthand {
param([string]$s)
$segments = $s -split '\s*>\s*'
$result = @()
# Build nested groups from right to left
$innermost = $null
for ($i = $segments.Count - 1; $i -ge 0; $i--) {
$seg = $segments[$i].Trim()
$group = New-Object PSObject
$group | Add-Member -NotePropertyName "type" -NotePropertyValue "group"
if ($seg -match '^(?i)(details|детали)$') {
# Empty groupBy = detailed records
$group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @()
} elseif ($seg -match '^(.+)\[(.+)\]$') {
# Named group: "ИмяГруппы[Поле]"
$group | Add-Member -NotePropertyName "name" -NotePropertyValue $Matches[1].Trim()
$group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($Matches[2].Trim())
} else {
$group | Add-Member -NotePropertyName "groupBy" -NotePropertyValue @($seg)
}
if ($null -ne $innermost) {
$group | Add-Member -NotePropertyName "children" -NotePropertyValue @($innermost)
}
$innermost = $group
}
if ($innermost) { $result += $innermost }
return ,$result
}
function Emit-StructureItem {
param($item, [string]$indent)
$type = if ($item.type) { "$($item.type)" } else { "group" }
if ($type -eq "group") {
X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">"
if ($item.name) {
X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>"
}
$gb = if ($item.groupBy) { $item.groupBy } else { $item.groupFields }
Emit-GroupItems -groupBy $gb -indent "$indent`t"
# Default order to ["Auto"] if not specified
$orderItems = $item.order
if (-not $orderItems) { $orderItems = @("Auto") }
Emit-Order -items $orderItems -indent "$indent`t"
# Default selection to ["Auto"] if not specified
$selItems = $item.selection
if (-not $selItems) { $selItems = @("Auto") }
Emit-Selection -items $selItems -indent "$indent`t"
Emit-Filter -items $item.filter -indent "$indent`t"
if ($item.outputParameters) {
Emit-OutputParameters -params $item.outputParameters -indent "$indent`t"
}
# Nested children
if ($item.children) {
foreach ($child in $item.children) {
Emit-StructureItem -item $child -indent "$indent`t"
}
}
X "$indent</dcsset:item>"
}
elseif ($type -eq "table") {
X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemTable`">"
if ($item.name) {
X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>"
}
# Columns
if ($item.columns) {
foreach ($col in $item.columns) {
X "$indent`t<dcsset:column>"
$colGb = if ($col.groupBy) { $col.groupBy } else { $col.groupFields }
Emit-GroupItems -groupBy $colGb -indent "$indent`t`t"
$colOrder = $col.order; if (-not $colOrder) { $colOrder = @("Auto") }
Emit-Order -items $colOrder -indent "$indent`t`t"
$colSel = $col.selection; if (-not $colSel) { $colSel = @("Auto") }
Emit-Selection -items $colSel -indent "$indent`t`t"
X "$indent`t</dcsset:column>"
}
}
# Rows
if ($item.rows) {
foreach ($row in $item.rows) {
X "$indent`t<dcsset:row>"
if ($row.name) {
X "$indent`t`t<dcsset:name>$(Esc-Xml "$($row.name)")</dcsset:name>"
}
$rowGb = if ($row.groupBy) { $row.groupBy } else { $row.groupFields }
Emit-GroupItems -groupBy $rowGb -indent "$indent`t`t"
$rowOrder = $row.order; if (-not $rowOrder) { $rowOrder = @("Auto") }
Emit-Order -items $rowOrder -indent "$indent`t`t"
$rowSel = $row.selection; if (-not $rowSel) { $rowSel = @("Auto") }
Emit-Selection -items $rowSel -indent "$indent`t`t"
X "$indent`t</dcsset:row>"
}
}
X "$indent</dcsset:item>"
}
elseif ($type -eq "chart") {
X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemChart`">"
if ($item.name) {
X "$indent`t<dcsset:name>$(Esc-Xml "$($item.name)")</dcsset:name>"
}
# Points
if ($item.points) {
X "$indent`t<dcsset:point>"
$ptGb = if ($item.points.groupBy) { $item.points.groupBy } else { $item.points.groupFields }
Emit-GroupItems -groupBy $ptGb -indent "$indent`t`t"
$ptOrder = $item.points.order; if (-not $ptOrder) { $ptOrder = @("Auto") }
Emit-Order -items $ptOrder -indent "$indent`t`t"
$ptSel = $item.points.selection; if (-not $ptSel) { $ptSel = @("Auto") }
Emit-Selection -items $ptSel -indent "$indent`t`t"
X "$indent`t</dcsset:point>"
}
# Series
if ($item.series) {
X "$indent`t<dcsset:series>"
$srGb = if ($item.series.groupBy) { $item.series.groupBy } else { $item.series.groupFields }
Emit-GroupItems -groupBy $srGb -indent "$indent`t`t"
$srOrder = $item.series.order; if (-not $srOrder) { $srOrder = @("Auto") }
Emit-Order -items $srOrder -indent "$indent`t`t"
$srSel = $item.series.selection; if (-not $srSel) { $srSel = @("Auto") }
Emit-Selection -items $srSel -indent "$indent`t`t"
X "$indent`t</dcsset:series>"
}
# Selection (chart values)
Emit-Selection -items $item.selection -indent "$indent`t"
if ($item.outputParameters) {
Emit-OutputParameters -params $item.outputParameters -indent "$indent`t"
}
X "$indent</dcsset:item>"
}
}
function Emit-SettingsVariants {
$variants = $def.settingsVariants
# Default variant if none specified
if (-not $variants -or $variants.Count -eq 0) {
$variants = @(@{
name = "Основной"
presentation = "Основной"
settings = @{
selection = @("Auto")
structure = @(@{
type = "group"
order = @("Auto")
selection = @("Auto")
})
}
})
# Convert to PSCustomObject-like structure
$variants = @($variants | ForEach-Object {
$v = New-Object PSObject
$v | Add-Member -NotePropertyName "name" -NotePropertyValue $_.name
$v | Add-Member -NotePropertyName "presentation" -NotePropertyValue $_.presentation
$settingsObj = New-Object PSObject
$settingsObj | Add-Member -NotePropertyName "selection" -NotePropertyValue $_.settings.selection
$structItem = New-Object PSObject
$structItem | Add-Member -NotePropertyName "type" -NotePropertyValue "group"
$structItem | Add-Member -NotePropertyName "order" -NotePropertyValue @("Auto")
$structItem | Add-Member -NotePropertyName "selection" -NotePropertyValue @("Auto")
$settingsObj | Add-Member -NotePropertyName "structure" -NotePropertyValue @($structItem)
$v | Add-Member -NotePropertyName "settings" -NotePropertyValue $settingsObj
$v
})
}
foreach ($v in $variants) {
X "`t<settingsVariant>"
X "`t`t<dcsset:name>$(Esc-Xml "$($v.name)")</dcsset:name>"
$pres = if ($v.presentation) { "$($v.presentation)" } elseif ($v.title) { "$($v.title)" } else { "$($v.name)" }
X "`t`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">"
X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t<v8:content>$(Esc-Xml $pres)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</dcsset:presentation>"
X "`t`t<dcsset:settings xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`">"
$s = $v.settings
# Selection (Auto items only belong at group level, not top-level settings)
if ($s.selection) {
Emit-Selection -items $s.selection -indent "`t`t`t" -skipAuto
}
# Filter
if ($s.filter) {
Emit-Filter -items $s.filter -indent "`t`t`t"
}
# Order (Auto items only belong at group level, not top-level settings)
if ($s.order) {
Emit-Order -items $s.order -indent "`t`t`t" -skipAuto
}
# ConditionalAppearance
if ($s.conditionalAppearance) {
Emit-ConditionalAppearance -items $s.conditionalAppearance -indent "`t`t`t"
}
# OutputParameters
if ($s.outputParameters) {
Emit-OutputParameters -params $s.outputParameters -indent "`t`t`t"
}
# DataParameters
if ($s.dataParameters -eq 'auto') {
# Auto-generate dataParameters for all non-hidden params
$autoDP = @()
foreach ($ap in $script:allParams) {
if (-not $ap.hidden) {
$dpItem = New-Object PSObject
$dpItem | Add-Member -NotePropertyName "parameter" -NotePropertyValue $ap.name
$dpItem | Add-Member -NotePropertyName "use" -NotePropertyValue $false
$dpItem | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue "auto"
$autoDP += $dpItem
}
}
if ($autoDP.Count -gt 0) {
Emit-DataParameters -items $autoDP -indent "`t`t`t"
}
} elseif ($s.dataParameters) {
Emit-DataParameters -items $s.dataParameters -indent "`t`t`t"
}
# Structure (supports string shorthand: "Организация > details")
if ($s.structure) {
$structItems = $s.structure
if ($structItems -is [string]) {
$structItems = Parse-StructureShorthand $structItems
}
foreach ($item in $structItems) {
Emit-StructureItem -item $item -indent "`t`t`t"
}
}
X "`t`t</dcsset:settings>"
X "`t</settingsVariant>"
}
}
# --- 12. Assemble XML ---
X "<?xml version=`"1.0`" encoding=`"UTF-8`"?>"
X "<DataCompositionSchema xmlns=`"http://v8.1c.ru/8.1/data-composition-system/schema`""
X "`t`txmlns:dcscom=`"http://v8.1c.ru/8.1/data-composition-system/common`""
X "`t`txmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`""
X "`t`txmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`""
X "`t`txmlns:v8=`"http://v8.1c.ru/8.1/data/core`""
X "`t`txmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`""
X "`t`txmlns:xs=`"http://www.w3.org/2001/XMLSchema`""
X "`t`txmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`">"
Emit-DataSources
Emit-DataSets
Emit-DataSetLinks
Emit-CalcFields
Emit-TotalFields
Emit-Parameters
Emit-Templates
Emit-GroupTemplates
Emit-SettingsVariants
X '</DataCompositionSchema>'
# --- 13. Write output ---
$parentDir = [System.IO.Path]::GetDirectoryName($OutputPath)
if ($parentDir -and -not (Test-Path $parentDir)) {
New-Item -ItemType Directory -Force $parentDir | Out-Null
}
$content = $script:xml.ToString()
$utf8Bom = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText($OutputPath, $content, $utf8Bom)
# --- 14. Statistics ---
$dsCount = $def.dataSets.Count
$fieldCount = 0
foreach ($ds in $def.dataSets) {
if ($ds.fields) { $fieldCount += $ds.fields.Count }
}
$calcCount = if ($def.calculatedFields) { $def.calculatedFields.Count } else { 0 }
$totalCount = if ($def.totalFields) { $def.totalFields.Count } else { 0 }
$paramCount = if ($def.parameters) { $def.parameters.Count } else { 0 }
$variantCount = if ($def.settingsVariants) { $def.settingsVariants.Count } else { 1 }
$fileSize = (Get-Item $OutputPath).Length
Write-Host "OK $OutputPath"
Write-Host " DataSets: $dsCount Fields: $fieldCount Calculated: $calcCount Totals: $totalCount Params: $paramCount Variants: $variantCount"
Write-Host " Size: $fileSize bytes"