# skd-compile v1.22 — 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('&','&').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) 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["стандартныйпериод"] = "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 or string(N) if ($typeStr -match '^string(\((\d+)\))?$') { $len = if ($Matches[2]) { $Matches[2] } else { "0" } X "$indentxs:string" X "$indent" X "$indent`t$len" X "$indent`tVariable" X "$indent" 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 "$indentxs:decimal" X "$indent" X "$indent`t$digits" X "$indent`t$fraction" X "$indent`t$sign" X "$indent" return } # date / dateTime if ($typeStr -match '^(date|dateTime)$') { $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } } 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 } # 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]@{} } # 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) # 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" } # --- 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 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]@{} } # 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" 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 if ($f.roles.Count -gt 0 -or $f["roleObj"]) { X "$indent`t" foreach ($role in $f.roles) { if ($role -eq "period") { # @period -> periodNumber + periodType (not ) X "$indent`t`t1" X "$indent`t`tMain" } else { X "$indent`t`ttrue" } } if ($f["roleObj"]) { $ro = $f["roleObj"] if ($ro.accountTypeExpression) { X "$indent`t`t$(Esc-Xml "$($ro.accountTypeExpression)")" } if ($ro.balanceGroup) { X "$indent`t`t$(Esc-Xml "$($ro.balanceGroup)")" } } X "$indent`t" } # ValueType if ($f.type) { X "$indent`t" Emit-ValueType -typeStr $f.type -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] X "$indent`t`t" X "$indent`t`t`t$(Esc-Xml $key)" if ($key -eq "ГоризонтальноеПоложение") { X "$indent`t`t`t$(Esc-Xml $val)" } else { X "$indent`t`t`t$(Esc-Xml $val)" } X "$indent`t`t" } X "$indent`t" } # PresentationExpression if ($f["presentationExpression"]) { X "$indent`t$(Esc-Xml $f["presentationExpression"])" } X "$indent" } # === 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" 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 items are nested dataSets Emit-DataSet -ds $item -indent "$indent`t" | 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)")" } 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) { 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" } 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 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`ttrue" } # 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) { $avVal = "$($av.value)" $avType = "xs:string" if ($avVal -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $avType = "dcscor:DesignTimeValue" } X "`t`t" 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)" } 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 { $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 } } # @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) 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" X "$indent`t$(Esc-Xml $valStr)" X "$indent`t0001-01-01T00:00:00" X "$indent`t0001-01-01T00:00:00" 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 = @{ 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 "$indentd8p1:$styleName" } else { 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" X "`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 $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" } 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" } # === 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" } } } # === 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$(Esc-Xml "$($gt.groupName)")" } elseif ($gt.groupField) { X "`t`t$(Esc-Xml "$($gt.groupField)")" } X "`t`t$(Esc-Xml $xmlTType)" X "`t`t" X "`t" } } # === Settings Variants === function Emit-Selection { param($items, [string]$indent, [switch]$skipAuto) if (-not $items -or $items.Count -eq 0) { return } X "$indent" foreach ($item in $items) { if ($item -is [string]) { if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t" } } else { X "$indent`t" X "$indent`t`t$(Esc-Xml $item)" X "$indent`t" } } elseif ($item.folder) { X "$indent`t" X "$indent`t`t" X "$indent`t`t`t" X "$indent`t`t`t`tru" X "$indent`t`t`t`t$(Esc-Xml "$($item.folder)")" X "$indent`t`t`t" X "$indent`t`t" foreach ($sub in $item.items) { $subName = if ($sub -is [string]) { $sub } else { "$($sub.field)" } X "$indent`t`t" X "$indent`t`t`t$(Esc-Xml $subName)" X "$indent`t`t" } X "$indent`t`tAuto" X "$indent`t" } else { X "$indent`t" X "$indent`t`t$(Esc-Xml "$($item.field)")" if ($item.title) { X "$indent`t`t" X "$indent`t`t`t" X "$indent`t`t`t`tru" X "$indent`t`t`t`t$(Esc-Xml "$($item.title)")" X "$indent`t`t`t" X "$indent`t`t" } X "$indent`t" } } X "$indent" } 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" X "$indent`t$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" return } # FilterItemComparison X "$indent" if ($item.use -eq $false) { X "$indent`tfalse" } X "$indent`t$(Esc-Xml "$($item.field)")" $compType = $script:comparisonTypes["$($item.op)"] if (-not $compType) { $compType = "$($item.op)" } X "$indent`t$(Esc-Xml $compType)" # 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$vStr" } if ($item.presentation) { Emit-MLText -tag "dcsset:presentation" -text $item.presentation -indent "$indent`t" } if ($item.viewMode) { X "$indent`t$(Esc-Xml "$($item.viewMode)")" } if ($item.userSettingID) { $uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" } X "$indent`t$(Esc-Xml $uid)" } if ($item.userSettingPresentation) { Emit-MLText -tag "dcsset:userSettingPresentation" -text $item.userSettingPresentation -indent "$indent`t" } X "$indent" } function Emit-Filter { param($items, [string]$indent) if (-not $items -or $items.Count -eq 0) { return } X "$indent" 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" } function Emit-Order { param($items, [string]$indent, [switch]$skipAuto) if (-not $items -or $items.Count -eq 0) { return } X "$indent" foreach ($item in $items) { if ($item -is [string]) { if ($item -eq "Auto") { if (-not $skipAuto) { X "$indent`t" } } 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" X "$indent`t`t$(Esc-Xml $field)" X "$indent`t`t$dir" X "$indent`t" } } } X "$indent" } function Emit-AppearanceValue { param([string]$key, $val, [string]$indent) X "$indent" if ($val -is [PSCustomObject] -and $val.use -ne $null -and $val.use -eq $false) { X "$indent`tfalse" X "$indent`t$(Esc-Xml $key)" $actualVal = "$($val.value)" } else { X "$indent`t$(Esc-Xml $key)" $actualVal = "$val" } # Auto-detect value type if ($actualVal -match '^(style|web|win):') { X "$indent`t$(Esc-Xml $actualVal)" } elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t$actualVal" } elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") { Emit-MLText -tag "dcscor:value" -text $actualVal -indent "$indent`t" } else { X "$indent`t$(Esc-Xml $actualVal)" } X "$indent" } function Emit-ConditionalAppearance { param($items, [string]$indent) if (-not $items -or $items.Count -eq 0) { return } X "$indent" foreach ($ca in $items) { X "$indent`t" # Selection (which fields to apply to; empty = all) if ($ca.selection -and $ca.selection.Count -gt 0) { X "$indent`t`t" foreach ($sel in $ca.selection) { X "$indent`t`t`t" X "$indent`t`t`t`t$(Esc-Xml "$sel")" X "$indent`t`t`t" } X "$indent`t`t" } else { X "$indent`t`t" } # 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" foreach ($prop in $ca.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$indent`t`t`t" } X "$indent`t`t" } # Presentation if ($ca.presentation) { X "$indent`t`t$(Esc-Xml "$($ca.presentation)")" } # ViewMode if ($ca.viewMode) { X "$indent`t`t$(Esc-Xml "$($ca.viewMode)")" } # UserSettingID if ($ca.userSettingID) { $uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" } X "$indent`t`t$(Esc-Xml $uid)" } X "$indent`t" } X "$indent" } function Emit-OutputParameters { param($params, [string]$indent) if (-not $params) { return } X "$indent" 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" X "$indent`t`t$(Esc-Xml $key)" if ($ptype -eq "mltext") { 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-DataParameters { param($items, [string]$indent) if (-not $items -or $items.Count -eq 0) { return } X "$indent" 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" if ($dp.use -eq $false) { X "$indent`t`tfalse" } X "$indent`t`t$(Esc-Xml "$($dp.parameter)")" # Value if ($dp.nilValue -eq $true) { X "$indent`t`t" } elseif ($null -ne $dp.value) { $vtype = "$($dp.valueType)" if ($dp.value -is [PSCustomObject] -and $dp.value.variant) { # StandardPeriod (object form from JSON) X "$indent`t`t" X "$indent`t`t`t$(Esc-Xml "$($dp.value.variant)")" X "$indent`t`t`t0001-01-01T00:00:00" X "$indent`t`t`t0001-01-01T00:00:00" X "$indent`t`t" } elseif ($dp.value -is [hashtable] -and $dp.value.variant) { # StandardPeriod (hashtable from shorthand parser) X "$indent`t`t" X "$indent`t`t`t$(Esc-Xml "$($dp.value.variant)")" X "$indent`t`t`t0001-01-01T00:00:00" X "$indent`t`t`t0001-01-01T00:00:00" X "$indent`t`t" } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { $bv = "$($dp.value)".ToLower() X "$indent`t`t$(Esc-Xml $bv)" } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { X "$indent`t`t$(Esc-Xml "$($dp.value)")" } elseif ($vtype -match '^decimal') { X "$indent`t`t$(Esc-Xml "$($dp.value)")" } elseif ($vtype -match '^string') { X "$indent`t`t$(Esc-Xml "$($dp.value)")" } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t$(Esc-Xml "$($dp.value)")" } else { X "$indent`t`t$(Esc-Xml "$($dp.value)")" } } if ($dp.viewMode) { X "$indent`t`t$(Esc-Xml "$($dp.viewMode)")" } if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" } X "$indent`t`t$(Esc-Xml $uid)" } if ($dp.userSettingPresentation) { Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t" } X "$indent`t" } X "$indent" } # === Structure items (recursive) === function Emit-GroupItems { param($groupBy, [string]$indent) if (-not $groupBy -or $groupBy.Count -eq 0) { return } X "$indent" foreach ($field in $groupBy) { if ($field -is [string]) { X "$indent`t" X "$indent`t`t$(Esc-Xml $field)" X "$indent`t`tItems" X "$indent`t`tNone" X "$indent`t`t0001-01-01T00:00:00" X "$indent`t`t0001-01-01T00:00:00" X "$indent`t" } else { # Object form X "$indent`t" X "$indent`t`t$(Esc-Xml "$($field.field)")" $gt = if ($field.groupType) { "$($field.groupType)" } else { "Items" } X "$indent`t`t$(Esc-Xml $gt)" $pat = if ($field.periodAdditionType) { "$($field.periodAdditionType)" } else { "None" } X "$indent`t`t$(Esc-Xml $pat)" X "$indent`t`t0001-01-01T00:00:00" X "$indent`t`t0001-01-01T00:00:00" X "$indent`t" } } X "$indent" } # 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" if ($item.name) { X "$indent`t$(Esc-Xml "$($item.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" } elseif ($type -eq "table") { X "$indent" if ($item.name) { X "$indent`t$(Esc-Xml "$($item.name)")" } # Columns if ($item.columns) { foreach ($col in $item.columns) { X "$indent`t" $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" } } # Rows if ($item.rows) { foreach ($row in $item.rows) { X "$indent`t" if ($row.name) { X "$indent`t`t$(Esc-Xml "$($row.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" } } X "$indent" } elseif ($type -eq "chart") { X "$indent" if ($item.name) { X "$indent`t$(Esc-Xml "$($item.name)")" } # Points if ($item.points) { X "$indent`t" $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" } # Series if ($item.series) { X "$indent`t" $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" } # Selection (chart values) Emit-Selection -items $item.selection -indent "$indent`t" if ($item.outputParameters) { Emit-OutputParameters -params $item.outputParameters -indent "$indent`t" } X "$indent" } } 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" X "`t`t$(Esc-Xml "$($v.name)")" $pres = if ($v.presentation) { $v.presentation } elseif ($v.title) { $v.title } else { "$($v.name)" } Emit-MLText -tag "dcsset:presentation" -text $pres -indent "`t`t" X "`t`t" $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. # Pattern follows 1C Designer / ERP persistence: # - value set (non-default) → emit value, use=true (implicit) # - value missing / Custom period → false + $autoDP = @() foreach ($ap in $script:allParams) { if ($ap.hidden) { continue } $dpItem = New-Object PSObject $dpItem | Add-Member -NotePropertyName "parameter" -NotePropertyValue $ap.name $dpItem | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue "auto" $hasMeaningfulValue = $false if ($ap.type -eq 'StandardPeriod') { # Inherit variant; Custom is treated as "empty" $variant = 'Custom' $av = $ap.value if ($null -ne $av) { if (($av -is [PSCustomObject] -or $av -is [hashtable]) -and $av.variant) { $variant = "$($av.variant)" } elseif ("$av") { $variant = "$av" } } $dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue @{ variant = $variant } if ($variant -ne 'Custom') { $hasMeaningfulValue = $true } } elseif ($null -ne $ap.value -and "$($ap.value)" -ne '') { $dpItem | Add-Member -NotePropertyName "value" -NotePropertyValue $ap.value $dpItem | Add-Member -NotePropertyName "valueType" -NotePropertyValue "$($ap.type)" $hasMeaningfulValue = $true } else { $dpItem | Add-Member -NotePropertyName "nilValue" -NotePropertyValue $true } if (-not $hasMeaningfulValue) { $dpItem | Add-Member -NotePropertyName "use" -NotePropertyValue $false } $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" X "`t" } } # --- 12. Assemble XML --- X "" X "" Emit-DataSources Emit-DataSets Emit-DataSetLinks Emit-CalcFields Emit-TotalFields Emit-Parameters Emit-Templates Emit-GroupTemplates Emit-SettingsVariants X '' # --- 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"