Files
cc-1c-skills/.codeassistant/skills/form-compile/scripts/form-compile.ps1
T
2026-06-14 11:08:54 +00:00

6565 lines
336 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# form-compile v1.172 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
[Parameter(Mandatory)]
[string]$OutputPath,
[switch]$FromObject,
[string]$ObjectPath,
[string]$Purpose,
[string]$Preset = "erp-standard",
[string]$EmitDsl
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# ═══════════════════════════════════════════════════════════════════════════
# FROM-OBJECT MODE: functions for metadata parsing, presets, DSL generation
# ═══════════════════════════════════════════════════════════════════════════
function Parse-ObjectMeta([string]$ObjectPath) {
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($ObjectPath)
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
# Detect object type from root child
$metaRoot = $doc.SelectSingleNode("md:MetaDataObject", $ns)
if (-not $metaRoot) { Write-Error "Not a 1C metadata XML: $ObjectPath"; exit 1 }
$typeNode = $metaRoot.FirstChild
$objType = $typeNode.LocalName # "Document", "Catalog", etc.
$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns)
$childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns)
# Name
$objName = $propsNode.SelectSingleNode("md:Name", $ns).InnerText
# Synonym (Russian)
$synonym = $objName
$synNode = $propsNode.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
if ($synNode) { $synonym = $synNode.InnerText }
# Helper: extract type string from md:Type
$extractType = {
param($typeParent)
if (-not $typeParent) { return "string" }
$types = @()
foreach ($t in $typeParent.SelectNodes("v8:Type", $ns)) {
$types += $t.InnerText
}
if ($types.Count -eq 0) { return "string" }
return ($types -join " | ")
}
# Helper: check if type is a reference
$isRefType = {
param([string]$t)
return ($t -match 'Ref\.' -or $t -match 'ссылка\.')
}
# Helper: extract field list from ChildObjects by tag name (Attribute, Dimension, Resource, AccountingFlag, ExtDimensionAccountingFlag)
$extractFields = {
param($parentNode, [string]$tagName)
$result = @()
if (-not $parentNode) { return $result }
foreach ($fieldNode in $parentNode.SelectNodes("md:$tagName", $ns)) {
$fp = $fieldNode.SelectSingleNode("md:Properties", $ns)
$fName = $fp.SelectSingleNode("md:Name", $ns).InnerText
$fSynNode = $fp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
$fSyn = if ($fSynNode) { $fSynNode.InnerText } else { $fName }
$fTypeNode = $fp.SelectSingleNode("md:Type", $ns)
$fType = & $extractType $fTypeNode
$result += @{
Name = $fName
Synonym = $fSyn
Type = $fType
IsRef = (& $isRefType $fType)
}
}
return $result
}
# Attributes
$attributes = @(& $extractFields $childObjs "Attribute")
# Tabular sections
$tabularSections = @()
if ($childObjs) {
foreach ($tsNode in $childObjs.SelectNodes("md:TabularSection", $ns)) {
$tsp = $tsNode.SelectSingleNode("md:Properties", $ns)
$tsName = $tsp.SelectSingleNode("md:Name", $ns).InnerText
$tsSynNode = $tsp.SelectSingleNode("md:Synonym/v8:item[v8:lang='ru']/v8:content", $ns)
$tsSyn = if ($tsSynNode) { $tsSynNode.InnerText } else { $tsName }
$tsCo = $tsNode.SelectSingleNode("md:ChildObjects", $ns)
$tsCols = @(& $extractFields $tsCo "Attribute")
$tabularSections += @{
Name = $tsName
Synonym = $tsSyn
Columns = $tsCols
}
}
}
$meta = @{
Type = $objType
Name = $objName
Synonym = $synonym
Attributes = $attributes
TabularSections = $tabularSections
}
# Type-specific properties
switch ($objType) {
"Document" {
$ntNode = $propsNode.SelectSingleNode("md:NumberType", $ns)
$meta.NumberType = if ($ntNode) { $ntNode.InnerText } else { "String" }
}
"Catalog" {
$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
$hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns)
$meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true")
$htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
$meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" }
# Owners
$owners = @()
foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) {
$owners += $ow.InnerText
}
$meta.Owners = $owners
}
"InformationRegister" {
$meta.Dimensions = @(& $extractFields $childObjs "Dimension")
$meta.Resources = @(& $extractFields $childObjs "Resource")
$prdNode = $propsNode.SelectSingleNode("md:InformationRegisterPeriodicity", $ns)
$meta.Periodicity = if ($prdNode) { $prdNode.InnerText } else { "Nonperiodical" }
$wmNode = $propsNode.SelectSingleNode("md:WriteMode", $ns)
$meta.WriteMode = if ($wmNode) { $wmNode.InnerText } else { "Independent" }
}
"AccumulationRegister" {
$meta.Dimensions = @(& $extractFields $childObjs "Dimension")
$meta.Resources = @(& $extractFields $childObjs "Resource")
$rtNode = $propsNode.SelectSingleNode("md:RegisterType", $ns)
$meta.RegisterType = if ($rtNode) { $rtNode.InnerText } else { "Balances" }
}
"ChartOfCharacteristicTypes" {
$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
$hiNode = $propsNode.SelectSingleNode("md:Hierarchical", $ns)
$meta.Hierarchical = ($hiNode -and $hiNode.InnerText -eq "true")
$htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
$meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" }
$owners = @()
foreach ($ow in $propsNode.SelectNodes("md:Owners/xr:Item", $ns)) {
$owners += $ow.InnerText
}
$meta.Owners = $owners
$meta.HasValueType = $true
}
"ExchangePlan" {
$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
$meta.Hierarchical = $false
$meta.HierarchyType = $null
$meta.Owners = @()
}
"ChartOfAccounts" {
$clNode = $propsNode.SelectSingleNode("md:CodeLength", $ns)
$meta.CodeLength = if ($clNode) { [int]$clNode.InnerText } else { 0 }
$dlNode = $propsNode.SelectSingleNode("md:DescriptionLength", $ns)
$meta.DescriptionLength = if ($dlNode) { [int]$dlNode.InnerText } else { 0 }
$meta.Hierarchical = $true
$htNode = $propsNode.SelectSingleNode("md:HierarchyType", $ns)
$meta.HierarchyType = if ($htNode) { $htNode.InnerText } else { "HierarchyFoldersAndItems" }
$meta.Owners = @()
$maxEdNode = $propsNode.SelectSingleNode("md:MaxExtDimensionCount", $ns)
$meta.MaxExtDimensionCount = if ($maxEdNode) { [int]$maxEdNode.InnerText } else { 0 }
$meta.AccountingFlags = @(& $extractFields $childObjs "AccountingFlag")
$meta.ExtDimensionAccountingFlags = @(& $extractFields $childObjs "ExtDimensionAccountingFlag")
}
}
return $meta
}
function Load-Preset([string]$PresetName, [string]$ScriptDir) {
# Hardcoded defaults (ERP-oriented)
$defaults = @{
"document.item" = @{
header = @{ position = "insidePage"; layout = "2col"; distribute = "even"; dateTitle = "от" }
footer = @{ fields = @("Комментарий"); position = "insidePage" }
tabularSections = @{ container = "pages"; exclude = @("ДополнительныеРеквизиты"); lineNumber = $true }
additional = @{ position = "page"; layout = "2col"; bspGroup = $true }
fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
commandBar = "auto"
properties = @{ autoTitle = $false }
}
"document.list" = @{
columns = "all"; columnType = "labelField"; hiddenRef = $true
tableCommandBar = "none"; commandBar = "auto"
properties = @{}
}
"document.choice" = @{
basedOn = "document.list"
properties = @{ windowOpeningMode = "LockOwnerWindow" }
}
"catalog.item" = @{
header = @{ layout = "1col"; distribute = "left" }
codeDescription = @{ layout = "horizontal"; order = "descriptionFirst" }
parent = @{ title = "Входит в группу"; position = "afterCodeDescription" }
owner = @{ readOnly = $true; position = "first" }
tabularSections = @{ container = "inline"; exclude = @("ДополнительныеРеквизиты","Представления"); lineNumber = $true }
footer = @{ fields = @(); position = "none" }
additional = @{ position = "none"; bspGroup = $true }
fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
commandBar = "auto"
properties = @{}
}
"catalog.folder" = @{
parent = @{ title = "Входит в группу" }
properties = @{ windowOpeningMode = "LockOwnerWindow" }
}
"catalog.list" = @{
columns = "all"; columnType = "labelField"; hiddenRef = $true
tableCommandBar = "none"; commandBar = "auto"
properties = @{}
}
"catalog.choice" = @{
basedOn = "catalog.list"; choiceMode = $true
properties = @{ windowOpeningMode = "LockOwnerWindow" }
}
# ─── Register defaults ───
"informationRegister.record" = @{
fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
properties = @{ windowOpeningMode = "LockOwnerWindow" }
}
"informationRegister.list" = @{
columns = "all"; columnType = "labelField"
tableCommandBar = "none"; commandBar = "auto"
properties = @{}
}
"accumulationRegister.list" = @{
columns = "all"; columnType = "labelField"
tableCommandBar = "none"; commandBar = "auto"
properties = @{}
}
# ─── Catalog-like type defaults ───
"chartOfCharacteristicTypes.item" = @{ basedOn = "catalog.item" }
"chartOfCharacteristicTypes.folder" = @{ basedOn = "catalog.folder" }
"chartOfCharacteristicTypes.list" = @{ basedOn = "catalog.list" }
"chartOfCharacteristicTypes.choice" = @{ basedOn = "catalog.choice" }
"exchangePlan.item" = @{ basedOn = "catalog.item" }
"exchangePlan.list" = @{ basedOn = "catalog.list" }
"exchangePlan.choice" = @{ basedOn = "catalog.choice" }
# ─── ChartOfAccounts defaults ───
"chartOfAccounts.item" = @{
parent = @{ title = "Подчинен счету" }
fieldDefaults = @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } }
properties = @{}
}
"chartOfAccounts.folder" = @{
parent = @{ title = "Подчинен счету" }
properties = @{ windowOpeningMode = "LockOwnerWindow" }
}
"chartOfAccounts.list" = @{ basedOn = "catalog.list" }
"chartOfAccounts.choice" = @{ basedOn = "catalog.choice" }
}
# Deep merge helper
$deepMerge = {
param($base, $overlay)
if (-not $overlay) { return $base }
if (-not $base) { return $overlay }
$result = @{}
foreach ($k in $base.Keys) { $result[$k] = $base[$k] }
foreach ($k in $overlay.Keys) {
if ($result.ContainsKey($k) -and $result[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) {
$result[$k] = & $deepMerge $result[$k] $overlay[$k]
} else {
$result[$k] = $overlay[$k]
}
}
return $result
}
# Try built-in preset
$presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets"
$builtInPath = Join-Path $presetDir "$PresetName.json"
if (Test-Path $builtInPath) {
$presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json
# Convert PSCustomObject to hashtable recursively
$toHash = {
param($obj)
if ($obj -is [System.Management.Automation.PSCustomObject]) {
$h = @{}
foreach ($p in $obj.PSObject.Properties) {
$h[$p.Name] = & $toHash $p.Value
}
return $h
}
if ($obj -is [System.Object[]]) {
return @($obj | ForEach-Object { & $toHash $_ })
}
return $obj
}
$presetHash = & $toHash $presetJson
foreach ($k in @($presetHash.Keys)) {
$defaults[$k] = & $deepMerge $defaults[$k] $presetHash[$k]
}
}
# Try project-level preset (scan up from output path)
$scanDir = [System.IO.Path]::GetDirectoryName($script:outPathResolved)
while ($scanDir) {
$projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json"
if (Test-Path $projPreset) {
$projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json
$projHash = & $toHash $projJson
foreach ($k in @($projHash.Keys)) {
$defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k]
}
break
}
$parentDir = Split-Path $scanDir -Parent
if ($parentDir -eq $scanDir) { break }
$scanDir = $parentDir
}
# Resolve basedOn references
foreach ($k in @($defaults.Keys)) {
$sect = $defaults[$k]
if ($sect -is [hashtable] -and $sect.ContainsKey("basedOn")) {
$baseName = $sect["basedOn"]
if ($defaults.ContainsKey($baseName)) {
$merged = & $deepMerge $defaults[$baseName] $sect
$merged.Remove("basedOn")
$defaults[$k] = $merged
}
}
}
return $defaults
}
# --- Helper: build a field element DSL entry ---
# Non-displayable types — cannot be bound to form elements
$script:nonDisplayableTypes = @('v8:ValueStorage', 'ValueStorage', 'ХранилищеЗначения')
function Test-DisplayableType([string]$typeStr) {
foreach ($nd in $script:nonDisplayableTypes) {
if ($typeStr -match [regex]::Escape($nd)) { return $false }
}
return $true
}
function New-FieldElement {
param([string]$attrName, [string]$dataPath, [string]$attrType, [hashtable]$fieldDefaults, [hashtable]$extraProps)
$isRef = ($attrType -match 'Ref\.')
$isBool = ($attrType -match '^\s*xs:boolean\s*$' -or $attrType -eq 'boolean' -or $attrType -match 'Boolean')
# Determine element type
$elType = "input"
if ($isBool -and $fieldDefaults -and $fieldDefaults.boolean -and $fieldDefaults.boolean.element -eq "check") {
$elType = "check"
}
$el = [ordered]@{ $elType = $attrName; path = $dataPath }
# (ChoiceButton у ref-полей платформа выводит сама; компилятор эмитит true по StartChoice-эвристике.
# Явный choiceButton из декомпиляции эмитится verbatim. Дефолт-«true» здесь НЕ ставим, чтобы
# from-object вывод совпадал с сертифицированным и не плодил ChoiceButton на каждом ref-поле.)
# Extra props
if ($extraProps) {
foreach ($k in $extraProps.Keys) { $el[$k] = $extraProps[$k] }
}
return $el
}
# --- Catalog DSL generators ---
function Generate-CatalogDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
$purposeKey = "catalog.$($purpose.ToLower())"
$p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} }
$fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} }
switch ($purpose) {
"Folder" { return Generate-CatalogFolderDSL $meta $p }
"List" { return Generate-CatalogListDSL $meta $p }
"Choice" { return Generate-CatalogChoiceDSL $meta $p $presetData }
"Item" { return Generate-CatalogItemDSL $meta $p $fd }
}
}
function Generate-CatalogFolderDSL($meta, [hashtable]$p) {
$elements = @()
# Code (if CodeLength > 0)
if ($meta.CodeLength -gt 0) {
$elements += [ordered]@{ input = "Код"; path = "Объект.Code" }
}
# Description
$elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
# Parent
$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null }
$parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" }
if ($parentTitle) { $parentEl["title"] = $parentTitle }
$elements += $parentEl
$props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" }
if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }
$formProps = [ordered]@{ useForFoldersAndItems = "Folders" }
foreach ($k in $props.Keys) { $formProps[$k] = $props[$k] }
return [ordered]@{
title = $meta.Synonym
properties = $formProps
elements = $elements
attributes = @(
[ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true }
)
}
}
function Generate-CatalogListDSL($meta, [hashtable]$p) {
# Columns
$columns = @()
# Description always first
$columns += [ordered]@{ labelField = "Наименование"; path = "Список.Description" }
# Code if present
if ($meta.CodeLength -gt 0) {
$columns += [ordered]@{ labelField = "Код"; path = "Список.Code" }
}
# Custom attributes
foreach ($attr in $meta.Attributes) {
if (-not (Test-DisplayableType $attr.Type)) { continue }
$columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" }
}
# Hidden ref
if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) {
$columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false }
}
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
}
# Hierarchical properties
if ($meta.Hierarchical) {
$tableEl["initialTreeView"] = "ExpandTopLevel"
$tableEl["enableStartDrag"] = $true
$tableEl["enableDrag"] = $true
}
$formProps = [ordered]@{}
if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $formProps
elements = @($tableEl)
attributes = @(
[ordered]@{
name = "Список"; type = "DynamicList"; main = $true
settings = [ordered]@{ mainTable = "Catalog.$($meta.Name)"; dynamicDataRead = $true }
}
)
}
}
function Generate-CatalogChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) {
# Start from list
$listKey = "catalog.list"
$lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} }
$dsl = Generate-CatalogListDSL $meta $lp
# Add choice-specific properties
$dsl.properties["windowOpeningMode"] = "LockOwnerWindow"
if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } }
# Set ChoiceMode on table
$dsl.elements[0]["choiceMode"] = $true
return $dsl
}
function Generate-CatalogItemDSL($meta, [hashtable]$p, [hashtable]$fd) {
$headerChildren = @()
# Owner (if subordinate)
if ($meta.Owners -and $meta.Owners.Count -gt 0) {
$ownerEl = [ordered]@{ input = "Владелец"; path = "Объект.Owner"; readOnly = $true }
$headerChildren += $ownerEl
}
# Code + Description
$cdLayout = if ($p.codeDescription -and $p.codeDescription.layout) { $p.codeDescription.layout } else { "horizontal" }
$cdOrder = if ($p.codeDescription -and $p.codeDescription.order) { $p.codeDescription.order } else { "descriptionFirst" }
$hasCode = ($meta.CodeLength -gt 0)
if ($cdLayout -eq "horizontal" -and $hasCode) {
$cdChildren = @()
$descEl = [ordered]@{ input = "Наименование"; path = "Объект.Description" }
$codeEl = [ordered]@{ input = "Код"; path = "Объект.Code" }
if ($cdOrder -eq "descriptionFirst") {
$cdChildren = @($descEl, $codeEl)
} else {
$cdChildren = @($codeEl, $descEl)
}
$headerChildren += [ordered]@{
group = "horizontal"; name = "ГруппаКодНаименование"; showTitle = $false
representation = "none"; children = $cdChildren
}
} else {
# Vertical or no code
$headerChildren += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
if ($hasCode) {
$headerChildren += [ordered]@{ input = "Код"; path = "Объект.Code" }
}
}
# Parent (for hierarchical catalogs)
$parentPos = if ($p.parent -and $p.parent.position) { $p.parent.position } else { "afterCodeDescription" }
$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { $null }
if ($meta.Hierarchical) {
$parentEl = [ordered]@{ input = "Родитель"; path = "Объект.Parent" }
if ($parentTitle) { $parentEl["title"] = $parentTitle }
if ($parentPos -eq "beforeCodeDescription") {
# Insert before Code/Description (after Owner if present)
$insertIdx = if ($meta.Owners -and $meta.Owners.Count -gt 0) { 1 } else { 0 }
$newChildren = @()
for ($i = 0; $i -lt $headerChildren.Count; $i++) {
if ($i -eq $insertIdx) { $newChildren += $parentEl }
$newChildren += $headerChildren[$i]
}
$headerChildren = $newChildren
} else {
# afterCodeDescription (default)
$headerChildren += $parentEl
}
}
# Custom attributes → header
$footerFieldNames = @()
if ($p.footer -and $p.footer.fields) { $footerFieldNames = @($p.footer.fields) }
foreach ($attr in $meta.Attributes) {
if ($footerFieldNames -contains $attr.Name) { continue }
if (-not (Test-DisplayableType $attr.Type)) { continue }
$headerChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{})
}
# Build root elements
$rootElements = @()
# ГруппаШапка
$rootElements += [ordered]@{
group = "vertical"; name = "ГруппаШапка"; showTitle = $false
representation = "none"; children = $headerChildren
}
# Tabular sections
$tsExclude = @("ДополнительныеРеквизиты", "Представления")
if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) }
$tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true }
$tsContainer = if ($p.tabularSections -and $p.tabularSections.container) { $p.tabularSections.container } else { "inline" }
$visibleTS = @()
foreach ($ts in $meta.TabularSections) {
if ($tsExclude -contains $ts.Name) { continue }
$visibleTS += $ts
}
foreach ($ts in $visibleTS) {
$tsCols = @()
if ($tsLineNumber) {
$tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" }
}
foreach ($col in $ts.Columns) {
$colEl = New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{}
$tsCols += $colEl
}
$tableEl = [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols }
$rootElements += $tableEl
}
# Footer fields
foreach ($fn in $footerFieldNames) {
$fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn }
if ($fAttr) {
$rootElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{})
}
}
# BSP group
$bspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true }
if ($bspGroup) {
$rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" }
}
# Properties
$formProps = [ordered]@{}
if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }
# UseForFoldersAndItems
if ($meta.Hierarchical -and $meta.HierarchyType -eq "HierarchyFoldersAndItems") {
$formProps["useForFoldersAndItems"] = "Items"
}
return [ordered]@{
title = $meta.Synonym
properties = $formProps
elements = $rootElements
attributes = @(
[ordered]@{ name = "Объект"; type = "CatalogObject.$($meta.Name)"; main = $true }
)
}
}
# --- Document DSL generators ---
function Generate-DocumentDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
$purposeKey = "document.$($purpose.ToLower())"
$p = if ($presetData.ContainsKey($purposeKey)) { $presetData[$purposeKey] } else { @{} }
$fd = if ($p.ContainsKey("fieldDefaults")) { $p.fieldDefaults } else { @{} }
switch ($purpose) {
"List" { return Generate-DocumentListDSL $meta $p }
"Choice" { return Generate-DocumentChoiceDSL $meta $p $presetData }
"Item" { return Generate-DocumentItemDSL $meta $p $fd }
}
}
function Generate-DocumentListDSL($meta, [hashtable]$p) {
$columns = @()
# Standard columns: Number + Date
$columns += [ordered]@{ labelField = "Номер"; path = "Список.Number" }
$columns += [ordered]@{ labelField = "Дата"; path = "Список.Date" }
# All custom attributes as labelField
foreach ($attr in $meta.Attributes) {
if (-not (Test-DisplayableType $attr.Type)) { continue }
$columns += [ordered]@{ labelField = $attr.Name; path = "Список.$($attr.Name)" }
}
# Hidden ref
if (-not $p.ContainsKey("hiddenRef") -or $p.hiddenRef -eq $true) {
$columns += [ordered]@{ labelField = "Ссылка"; path = "Список.Ref"; userVisible = $false }
}
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
}
$formProps = [ordered]@{}
if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $formProps
elements = @($tableEl)
attributes = @(
[ordered]@{
name = "Список"; type = "DynamicList"; main = $true
settings = [ordered]@{ mainTable = "Document.$($meta.Name)"; dynamicDataRead = $true }
}
)
}
}
function Generate-DocumentChoiceDSL($meta, [hashtable]$p, [hashtable]$presetData) {
$listKey = "document.list"
$lp = if ($presetData.ContainsKey($listKey)) { $presetData[$listKey] } else { @{} }
$dsl = Generate-DocumentListDSL $meta $lp
$dsl.properties["windowOpeningMode"] = "LockOwnerWindow"
if ($p.properties) { foreach ($k in $p.properties.Keys) { $dsl.properties[$k] = $p.properties[$k] } }
return $dsl
}
function Generate-DocumentItemDSL($meta, [hashtable]$p, [hashtable]$fd) {
$headerPos = if ($p.header -and $p.header.position) { $p.header.position } else { "insidePage" }
$headerLayout = if ($p.header -and $p.header.layout) { $p.header.layout } else { "2col" }
$headerDistribute = if ($p.header -and $p.header.distribute) { $p.header.distribute } else { "even" }
$dateTitle = if ($p.header -and $p.header.dateTitle) { $p.header.dateTitle } else { "от" }
$footerFields = @()
if ($p.footer -and $p.footer.fields) { $footerFields = @($p.footer.fields) }
$footerPos = if ($p.footer -and $p.footer.position) { $p.footer.position } else { "insidePage" }
$addPos = if ($p.additional -and $p.additional.position) { $p.additional.position } else { "page" }
$addLayout = if ($p.additional -and $p.additional.layout) { $p.additional.layout } else { "2col" }
$addBspGroup = if ($p.additional -and $null -ne $p.additional.bspGroup) { $p.additional.bspGroup } else { $true }
$addLeft = @(); $addRight = @()
if ($p.additional -and $p.additional.left) { $addLeft = @($p.additional.left) }
if ($p.additional -and $p.additional.right) { $addRight = @($p.additional.right) }
$headerRight = @()
if ($p.header -and $p.header.right) { $headerRight = @($p.header.right) }
$tsExclude = @("ДополнительныеРеквизиты")
if ($p.tabularSections -and $p.tabularSections.exclude) { $tsExclude = @($p.tabularSections.exclude) }
$tsLineNumber = if ($p.tabularSections -and $null -ne $p.tabularSections.lineNumber) { $p.tabularSections.lineNumber } else { $true }
# Classify attributes
$claimed = @{}
foreach ($fn in $footerFields) { $claimed[$fn] = "footer" }
foreach ($fn in $headerRight) { $claimed[$fn] = "header.right" }
foreach ($fn in $addLeft) { $claimed[$fn] = "additional.left" }
foreach ($fn in $addRight) { $claimed[$fn] = "additional.right" }
$unclaimed = @()
foreach ($attr in $meta.Attributes) {
if (-not $claimed.ContainsKey($attr.Name) -and (Test-DisplayableType $attr.Type)) { $unclaimed += $attr }
}
# Distribute unclaimed
$leftAttrs = @(); $rightExtraAttrs = @()
switch ($headerDistribute) {
"left" { $leftAttrs = $unclaimed }
"right" { $rightExtraAttrs = $unclaimed }
default { # "even"
$half = [Math]::Ceiling($unclaimed.Count / 2)
for ($i = 0; $i -lt $unclaimed.Count; $i++) {
if ($i -lt $half) { $leftAttrs += $unclaimed[$i] }
else { $rightExtraAttrs += $unclaimed[$i] }
}
}
}
# Build ГруппаНомерДата
$numDateChildren = @(
[ordered]@{ input = "Номер"; path = "Объект.Number"; autoMaxWidth = $false; width = 9 }
[ordered]@{ input = "Дата"; path = "Объект.Date"; title = $dateTitle }
)
$numDateGroup = [ordered]@{
group = "horizontal"; name = "ГруппаНомерДата"; showTitle = $false; children = $numDateChildren
}
# Build left column
$leftChildren = @($numDateGroup)
foreach ($attr in $leftAttrs) {
$leftChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{})
}
# Build right column
$rightChildren = @()
foreach ($rn in $headerRight) {
$rAttr = $meta.Attributes | Where-Object { $_.Name -eq $rn }
if ($rAttr) {
$rightChildren += (New-FieldElement -attrName $rAttr.Name -dataPath "Объект.$($rAttr.Name)" -attrType $rAttr.Type -fieldDefaults $fd -extraProps @{})
}
}
foreach ($attr in $rightExtraAttrs) {
$rightChildren += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd -extraProps @{})
}
# Header group
$headerGroup = $null
if ($headerLayout -eq "2col" -and $rightChildren.Count -gt 0) {
$headerGroup = [ordered]@{
group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none"
children = @(
[ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $leftChildren }
[ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $rightChildren }
)
}
} else {
# 1col or no right items
$allHeaderFields = $leftChildren + $rightChildren
$headerGroup = [ordered]@{
group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none"
children = @(
[ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $allHeaderFields }
)
}
}
# Footer elements
$footerElements = @()
foreach ($fn in $footerFields) {
$fAttr = $meta.Attributes | Where-Object { $_.Name -eq $fn }
if ($fAttr -and (Test-DisplayableType $fAttr.Type)) {
$footerElements += (New-FieldElement -attrName $fAttr.Name -dataPath "Объект.$($fAttr.Name)" -attrType $fAttr.Type -fieldDefaults $fd -extraProps @{})
}
}
# Visible tabular sections
$visibleTS = @()
foreach ($ts in $meta.TabularSections) {
if ($tsExclude -contains $ts.Name) { continue }
$visibleTS += $ts
}
# Additional page content
$additionalPage = $null
if ($addPos -eq "page") {
$addLeftEls = @(); $addRightEls = @()
foreach ($aln in $addLeft) {
$alAttr = $meta.Attributes | Where-Object { $_.Name -eq $aln }
if ($alAttr) {
$addLeftEls += (New-FieldElement -attrName $alAttr.Name -dataPath "Объект.$($alAttr.Name)" -attrType $alAttr.Type -fieldDefaults $fd -extraProps @{})
}
}
foreach ($arn in $addRight) {
$arAttr = $meta.Attributes | Where-Object { $_.Name -eq $arn }
if ($arAttr) {
$addRightEls += (New-FieldElement -attrName $arAttr.Name -dataPath "Объект.$($arAttr.Name)" -attrType $arAttr.Type -fieldDefaults $fd -extraProps @{})
}
}
$addPageChildren = @()
if ($addLayout -eq "2col") {
$addPageChildren += [ordered]@{
group = "horizontal"; name = "ГруппаПараметры"; showTitle = $false
children = @(
[ordered]@{ group = "vertical"; name = "ГруппаПараметрыЛево"; showTitle = $false; children = $addLeftEls }
[ordered]@{ group = "vertical"; name = "ГруппаПараметрыПраво"; showTitle = $false; children = $addRightEls }
)
}
} else {
$addPageChildren += @($addLeftEls + $addRightEls)
}
if ($addBspGroup) {
$addPageChildren += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" }
}
$additionalPage = [ordered]@{ page = "ГруппаДополнительно"; title = "Дополнительно"; children = $addPageChildren }
}
# Build TS page elements
$tsPages = @()
foreach ($ts in $visibleTS) {
$tsCols = @()
if ($tsLineNumber) {
$tsCols += [ordered]@{ labelField = "$($ts.Name)НомерСтроки"; path = "Объект.$($ts.Name).LineNumber" }
}
foreach ($col in $ts.Columns) {
$tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd -extraProps @{})
}
$tsPages += [ordered]@{
page = "Группа$($ts.Name)"; title = $ts.Synonym
children = @(
[ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols }
)
}
}
# Assemble root elements
$rootElements = @()
if ($visibleTS.Count -eq 0) {
# Simple form — no Pages
$rootElements += $headerGroup
if ($footerElements.Count -gt 0) { $rootElements += $footerElements }
if ($addBspGroup -and $addPos -ne "none") {
$rootElements += [ordered]@{ group = "vertical"; name = "ГруппаДополнительныеРеквизиты" }
}
} else {
# Pages form
if ($headerPos -eq "abovePages") {
$rootElements += $headerGroup
$pagesChildren = @()
$pagesChildren += $tsPages
if ($additionalPage) { $pagesChildren += $additionalPage }
$rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren }
} else {
# insidePage (default)
$osnovnoeChildren = @($headerGroup)
if ($footerPos -eq "insidePage" -and $footerElements.Count -gt 0) {
$osnovnoeChildren += $footerElements
}
$pagesChildren = @()
$pagesChildren += [ordered]@{ page = "ГруппаОсновное"; title = "Основное"; children = $osnovnoeChildren }
$pagesChildren += $tsPages
if ($additionalPage) { $pagesChildren += $additionalPage }
$rootElements += [ordered]@{ pages = "ГруппаСтраницы"; children = $pagesChildren }
}
# Footer below pages
if ($footerPos -eq "belowPages" -and $footerElements.Count -gt 0) {
$rootElements += $footerElements
}
}
# Properties
$formProps = [ordered]@{ autoTitle = $false }
if ($p.properties) { foreach ($k in $p.properties.Keys) { $formProps[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $formProps
elements = $rootElements
attributes = @(
[ordered]@{ name = "Объект"; type = "DocumentObject.$($meta.Name)"; main = $true }
)
}
}
# ─── InformationRegister ──────────────────────────────────────────────────
function Generate-InformationRegisterDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
$pKey = "informationRegister.$($purpose.ToLower())"
$p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} }
$fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } }
switch ($purpose) {
"Record" { return Generate-InformationRegisterRecordDSL $meta $p $fd }
"List" { return Generate-InformationRegisterListDSL $meta $p }
}
}
function Generate-InformationRegisterRecordDSL($meta, [hashtable]$p, [hashtable]$fd) {
$elements = @()
$isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical"
# Period first (if periodic)
if ($isPeriodic) {
$elements += [ordered]@{ input = "Период"; path = "Запись.Period" }
}
# Dimensions
foreach ($dim in $meta.Dimensions) {
if (-not (Test-DisplayableType $dim.Type)) { continue }
$elements += (New-FieldElement -attrName $dim.Name -dataPath "Запись.$($dim.Name)" -attrType $dim.Type -fieldDefaults $fd)
}
# Resources
foreach ($res in $meta.Resources) {
if (-not (Test-DisplayableType $res.Type)) { continue }
$elements += (New-FieldElement -attrName $res.Name -dataPath "Запись.$($res.Name)" -attrType $res.Type -fieldDefaults $fd)
}
# Attributes
foreach ($attr in $meta.Attributes) {
if (-not (Test-DisplayableType $attr.Type)) { continue }
$elements += (New-FieldElement -attrName $attr.Name -dataPath "Запись.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd)
}
$props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" }
if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $props
elements = $elements
attributes = @(
@{ name = "Запись"; type = "InformationRegisterRecordManager.$($meta.Name)"; main = $true; savedData = $true }
)
}
}
function Generate-InformationRegisterListDSL($meta, [hashtable]$p) {
$isPeriodic = $meta.Periodicity -and $meta.Periodicity -ne "Nonperiodical"
$isRecorderSubordinate = $meta.WriteMode -eq "RecorderSubordinate"
$columns = @()
# Period
if ($isPeriodic) {
$columns += [ordered]@{ labelField = "Период"; path = "Список.Period" }
}
# Recorder/LineNumber for subordinate registers
if ($isRecorderSubordinate) {
$columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" }
$columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" }
}
# Dimensions
foreach ($dim in $meta.Dimensions) {
if (-not (Test-DisplayableType $dim.Type)) { continue }
$columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" }
}
# Resources
foreach ($res in $meta.Resources) {
if (-not (Test-DisplayableType $res.Type)) { continue }
$elKey = "labelField"
if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
$columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" }
}
# Attributes
foreach ($attr in $meta.Attributes) {
if (-not (Test-DisplayableType $attr.Type)) { continue }
$elKey = "labelField"
if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
$columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" }
}
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
}
$props = [ordered]@{}
if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $props
elements = @($tableEl)
attributes = @(
@{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "InformationRegister.$($meta.Name)"; dynamicDataRead = $true } }
)
}
}
# ─── AccumulationRegister ─────────────────────────────────────────────────
function Generate-AccumulationRegisterDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
$pKey = "accumulationRegister.$($purpose.ToLower())"
$p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} }
switch ($purpose) {
"List" { return Generate-AccumulationRegisterListDSL $meta $p }
}
}
function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) {
$columns = @()
# AccumulationRegisters always have Period, Recorder, LineNumber
$columns += [ordered]@{ labelField = "Период"; path = "Список.Period" }
$columns += [ordered]@{ labelField = "Регистратор"; path = "Список.Recorder" }
$columns += [ordered]@{ labelField = "НомерСтроки"; path = "Список.LineNumber" }
# Dimensions
foreach ($dim in $meta.Dimensions) {
if (-not (Test-DisplayableType $dim.Type)) { continue }
$columns += [ordered]@{ labelField = $dim.Name; path = "Список.$($dim.Name)" }
}
# Resources
foreach ($res in $meta.Resources) {
if (-not (Test-DisplayableType $res.Type)) { continue }
$elKey = "labelField"
if ($res.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
$columns += [ordered]@{ $elKey = $res.Name; path = "Список.$($res.Name)" }
}
# Attributes
foreach ($attr in $meta.Attributes) {
if (-not (Test-DisplayableType $attr.Type)) { continue }
$elKey = "labelField"
if ($attr.Type -match '^xs:boolean$|^Boolean$') { $elKey = "check" }
$columns += [ordered]@{ $elKey = $attr.Name; path = "Список.$($attr.Name)" }
}
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
}
$props = [ordered]@{}
if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $props
elements = @($tableEl)
attributes = @(
@{ name = "Список"; type = "DynamicList"; main = $true; settings = @{ mainTable = "AccumulationRegister.$($meta.Name)"; dynamicDataRead = $true } }
)
}
}
# ─── ChartOfCharacteristicTypes (delegates to Catalog) ────────────────────
function Generate-ChartOfCharacteristicTypesDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
# Delegate to Catalog generators — meta already has CodeLength, DescriptionLength, etc.
$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose
# Post-patch: replace Catalog types with ChartOfCharacteristicTypes types
$catObjType = "CatalogObject.$($meta.Name)"
$ccoctObjType = "ChartOfCharacteristicTypesObject.$($meta.Name)"
$catListType = "Catalog.$($meta.Name)"
$ccoctListType = "ChartOfCharacteristicTypes.$($meta.Name)"
foreach ($a in $dsl.attributes) {
if ($a.type -eq $catObjType) { $a.type = $ccoctObjType }
if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) {
$a.settings.mainTable = $ccoctListType
}
}
# For Item forms: inject ValueType field after Code/Description
if ($purpose -eq "Item" -and $dsl.elements) {
$vtEl = [ordered]@{ input = "ТипЗначения"; path = "Объект.ValueType" }
$newElements = @()
$inserted = $false
foreach ($el in $dsl.elements) {
$newElements += $el
if (-not $inserted) {
$elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" }
if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") {
$newElements += $vtEl
$inserted = $true
}
}
}
if (-not $inserted) { $newElements += $vtEl }
$dsl.elements = $newElements
}
return $dsl
}
# ─── ExchangePlan (delegates to Catalog) ──────────────────────────────────
function Generate-ExchangePlanDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
# ExchangePlans are not hierarchical and have no Folder form
$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $purpose
# Post-patch: replace Catalog types with ExchangePlan types
$catObjType = "CatalogObject.$($meta.Name)"
$epObjType = "ExchangePlanObject.$($meta.Name)"
$catListType = "Catalog.$($meta.Name)"
$epListType = "ExchangePlan.$($meta.Name)"
foreach ($a in $dsl.attributes) {
if ($a.type -eq $catObjType) { $a.type = $epObjType }
if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq $catListType) {
$a.settings.mainTable = $epListType
}
}
# For Item forms: inject SentNo, ReceivedNo after Code/Description
if ($purpose -eq "Item" -and $dsl.elements) {
$sentEl = [ordered]@{ input = "НомерОтправленного"; path = "Объект.SentNo"; readOnly = $true }
$recvEl = [ordered]@{ input = "НомерПринятого"; path = "Объект.ReceivedNo"; readOnly = $true }
$newElements = @()
$inserted = $false
foreach ($el in $dsl.elements) {
$newElements += $el
if (-not $inserted) {
$elName = if ($el.input) { $el.input } elseif ($el.name) { $el.name } elseif ($el.group) { $el.group } else { "" }
if ($elName -eq "Наименование" -or $elName -eq "ГруппаКодНаименование") {
$newElements += $sentEl
$newElements += $recvEl
$inserted = $true
}
}
}
if (-not $inserted) { $newElements += $sentEl; $newElements += $recvEl }
$dsl.elements = $newElements
}
return $dsl
}
# ─── ChartOfAccounts ──────────────────────────────────────────────────────
function Generate-ChartOfAccountsDSL {
param($meta, [hashtable]$presetData, [string]$purpose)
$pKey = "chartOfAccounts.$($purpose.ToLower())"
$p = if ($presetData.ContainsKey($pKey)) { $presetData[$pKey] } else { @{} }
$fd = if ($p.fieldDefaults) { $p.fieldDefaults } else { @{ ref = @{ choiceButton = $true }; boolean = @{ element = "check" } } }
switch ($purpose) {
"Item" { return Generate-ChartOfAccountsItemDSL $meta $p $fd $presetData }
"Folder" { return Generate-ChartOfAccountsFolderDSL $meta $p }
"List" { return Generate-ChartOfAccountsListDSL $meta $presetData }
"Choice" { return Generate-ChartOfAccountsChoiceDSL $meta $presetData }
}
}
function Generate-ChartOfAccountsItemDSL($meta, [hashtable]$p, [hashtable]$fd, [hashtable]$presetData) {
$elements = @()
# Header: Code + Parent
$headerLeftChildren = @()
if ($meta.CodeLength -gt 0) {
$headerLeftChildren += [ordered]@{ input = "Код"; path = "Объект.Code" }
}
$headerRightChildren = @()
if ($meta.Hierarchical) {
$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" }
$headerRightChildren += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle }
}
if ($headerRightChildren.Count -gt 0) {
$elements += [ordered]@{
group = "horizontal"; name = "ГруппаШапка"; showTitle = $false; representation = "none"
children = @(
[ordered]@{ group = "vertical"; name = "ГруппаШапкаЛево"; showTitle = $false; children = $headerLeftChildren }
[ordered]@{ group = "vertical"; name = "ГруппаШапкаПраво"; showTitle = $false; children = $headerRightChildren }
)
}
} elseif ($headerLeftChildren.Count -gt 0) {
$elements += $headerLeftChildren
}
# Description
if ($meta.DescriptionLength -gt 0) {
$elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
}
# OffBalance
$elements += [ordered]@{ check = "Забалансовый"; path = "Объект.OffBalance" }
# AccountingFlags as checkboxes
if ($meta.AccountingFlags -and $meta.AccountingFlags.Count -gt 0) {
$flagChildren = @()
foreach ($flag in $meta.AccountingFlags) {
$flagChildren += [ordered]@{ check = $flag.Name; path = "Объект.$($flag.Name)" }
}
$elements += [ordered]@{
group = "vertical"; name = "ГруппаПризнакиУчета"; title = "Признаки учета"
children = $flagChildren
}
}
# ExtDimensionTypes table
if ($meta.MaxExtDimensionCount -gt 0) {
# Имена колонок табчасти префиксуются именем таблицы (как generic-путь и типовая 1С),
# иначе флаг субконто (напр. "Валютный") столкнётся с одноимённым признаком учёта счёта.
$edTable = "ВидыСубконто"
$edCols = @()
$edCols += [ordered]@{ input = "${edTable}ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" }
$edCols += [ordered]@{ check = "${edTable}ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" }
if ($meta.ExtDimensionAccountingFlags) {
foreach ($edFlag in $meta.ExtDimensionAccountingFlags) {
$edCols += [ordered]@{ check = "${edTable}$($edFlag.Name)"; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" }
}
}
$elements += [ordered]@{
table = $edTable
path = "Объект.ExtDimensionTypes"
columns = $edCols
}
}
# Custom attributes
foreach ($attr in $meta.Attributes) {
if (-not (Test-DisplayableType $attr.Type)) { continue }
$elements += (New-FieldElement -attrName $attr.Name -dataPath "Объект.$($attr.Name)" -attrType $attr.Type -fieldDefaults $fd)
}
# Tabular sections
$tsExclude = @("ДополнительныеРеквизиты","Представления")
foreach ($ts in $meta.TabularSections) {
if ($tsExclude -contains $ts.Name) { continue }
$tsCols = @()
foreach ($col in $ts.Columns) {
if (-not (Test-DisplayableType $col.Type)) { continue }
$tsCols += (New-FieldElement -attrName "$($ts.Name)$($col.Name)" -dataPath "Объект.$($ts.Name).$($col.Name)" -attrType $col.Type -fieldDefaults $fd)
}
$elements += [ordered]@{ table = $ts.Name; path = "Объект.$($ts.Name)"; columns = $tsCols }
}
$props = [ordered]@{}
if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
properties = $props
elements = $elements
attributes = @(
@{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true }
)
}
}
function Generate-ChartOfAccountsFolderDSL($meta, [hashtable]$p) {
$elements = @()
if ($meta.CodeLength -gt 0) {
$elements += [ordered]@{ input = "Код"; path = "Объект.Code" }
}
if ($meta.DescriptionLength -gt 0) {
$elements += [ordered]@{ input = "Наименование"; path = "Объект.Description" }
}
if ($meta.Hierarchical) {
$parentTitle = if ($p.parent -and $p.parent.title) { $p.parent.title } else { "Подчинен счету" }
$elements += [ordered]@{ input = "Родитель"; path = "Объект.Parent"; title = $parentTitle }
}
$props = [ordered]@{ windowOpeningMode = "LockOwnerWindow" }
if ($p.properties) { foreach ($k in $p.properties.Keys) { $props[$k] = $p.properties[$k] } }
return [ordered]@{
title = $meta.Synonym
useForFoldersAndItems = "Folders"
properties = $props
elements = $elements
attributes = @(
@{ name = "Объект"; type = "ChartOfAccountsObject.$($meta.Name)"; main = $true; savedData = $true }
)
}
}
function Generate-ChartOfAccountsListDSL($meta, [hashtable]$presetData) {
# Delegate to Catalog List and patch types
$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "List"
foreach ($a in $dsl.attributes) {
if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") {
$a.settings.mainTable = "ChartOfAccounts.$($meta.Name)"
}
}
return $dsl
}
function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) {
$dsl = Generate-CatalogDSL -meta $meta -presetData $presetData -purpose "Choice"
foreach ($a in $dsl.attributes) {
if ($a.type -eq "DynamicList" -and $a.settings -and $a.settings.mainTable -eq "Catalog.$($meta.Name)") {
$a.settings.mainTable = "ChartOfAccounts.$($meta.Name)"
}
}
return $dsl
}
# ═══════════════════════════════════════════════════════════════════════════
# END OF FROM-OBJECT MODE FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════
# --- Detect XML format version ---
function Detect-FormatVersion([string]$dir) {
$d = $dir
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
# --- 0. Path normalization and mode dispatch ---
# Form name → purpose mapping
$script:formNameToPurpose = @{
"ФормаДокумента" = "Item"
"ФормаЭлемента" = "Item"
"ФормаСписка" = "List"
"ФормаВыбора" = "Choice"
"ФормаГруппы" = "Folder"
"ФормаЗаписи" = "Record"
"ФормаСчета" = "Item"
"ФормаУзла" = "Item"
}
if ($FromObject -and $JsonPath) {
Write-Error "Cannot use both -JsonPath and -FromObject. Choose one mode."
exit 1
}
if (-not $FromObject -and -not $JsonPath) {
Write-Error "Either -JsonPath or -FromObject is required."
exit 1
}
if ($FromObject) {
# Normalize OutputPath: append /Ext/Form.xml if missing
$outNorm = $OutputPath -replace '[\\/]$', ''
if ($outNorm -notmatch '[/\\]Ext[/\\]Form\.xml$') {
if ($outNorm -match '[/\\]Ext$') {
$OutputPath = "$outNorm/Form.xml"
} else {
$OutputPath = "$outNorm/Ext/Form.xml"
}
Write-Host "[resolved] OutputPath -> $OutputPath"
}
# Resolve object path and purpose from OutputPath convention:
# .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
$outAbs = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$pathParts = $outAbs -split '[/\\]'
# Find "Forms" segment
$formsIdx = -1
for ($i = $pathParts.Count - 1; $i -ge 0; $i--) {
if ($pathParts[$i] -eq "Forms") { $formsIdx = $i; break }
}
$resolvedObjectPath = $null
$resolvedPurpose = $null
if ($formsIdx -ge 2) {
$formName = $pathParts[$formsIdx + 1]
$objectName = $pathParts[$formsIdx - 1]
$typePluralAndAbove = $pathParts[0..($formsIdx - 2)] -join [IO.Path]::DirectorySeparatorChar
# Derive purpose from form name
if ($script:formNameToPurpose.ContainsKey($formName)) {
$resolvedPurpose = $script:formNameToPurpose[$formName]
}
# Derive object XML path
$candidateObjPath = Join-Path $typePluralAndAbove "$objectName.xml"
if (Test-Path $candidateObjPath) {
$resolvedObjectPath = $candidateObjPath
}
}
# Apply: explicit -ObjectPath / -Purpose override resolved values
$fromObjPath = $null
if ($ObjectPath) {
$fromObjPath = if ([System.IO.Path]::IsPathRooted($ObjectPath)) { $ObjectPath } else { Join-Path (Get-Location) $ObjectPath }
# Append .xml if missing
if (-not $fromObjPath.EndsWith(".xml")) { $fromObjPath = "$fromObjPath.xml" }
} elseif ($resolvedObjectPath) {
$fromObjPath = $resolvedObjectPath
Write-Host "[resolved] ObjectPath -> $fromObjPath"
} else {
Write-Error "Cannot derive object path from OutputPath. Use -ObjectPath explicitly."
exit 1
}
if (-not (Test-Path $fromObjPath)) {
Write-Error "Object file not found: $fromObjPath"
exit 1
}
$effectivePurpose = if ($Purpose) { $Purpose } elseif ($resolvedPurpose) { $resolvedPurpose } else { "Item" }
if ($resolvedPurpose -and -not $Purpose) {
Write-Host "[resolved] Purpose -> $effectivePurpose"
}
$meta = Parse-ObjectMeta $fromObjPath
Write-Host "[from-object] Type=$($meta.Type), Name=$($meta.Name), Attrs=$($meta.Attributes.Count), TS=$($meta.TabularSections.Count)"
$presetData = Load-Preset -PresetName $Preset -ScriptDir $PSScriptRoot
$supportedPurposes = switch ($meta.Type) {
"Document" { @("Item","List","Choice") }
"Catalog" { @("Item","Folder","List","Choice") }
"InformationRegister" { @("Record","List") }
"AccumulationRegister" { @("List") }
"ChartOfCharacteristicTypes" { @("Item","Folder","List","Choice") }
"ExchangePlan" { @("Item","List","Choice") }
"ChartOfAccounts" { @("Item","Folder","List","Choice") }
default { @() }
}
if ($supportedPurposes.Count -eq 0) {
Write-Error "Object type '$($meta.Type)' is not yet supported by --from-object. Supported: Document, Catalog, InformationRegister, AccumulationRegister, ChartOfCharacteristicTypes, ExchangePlan, ChartOfAccounts."
exit 1
}
if ($supportedPurposes -notcontains $effectivePurpose) {
Write-Error "Purpose '$effectivePurpose' is not valid for $($meta.Type). Valid: $($supportedPurposes -join ', ')"
exit 1
}
# Generate DSL
$dsl = switch ($meta.Type) {
"Document" { Generate-DocumentDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
"Catalog" { Generate-CatalogDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
"InformationRegister" { Generate-InformationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
"AccumulationRegister" { Generate-AccumulationRegisterDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
"ChartOfCharacteristicTypes" { Generate-ChartOfCharacteristicTypesDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
"ExchangePlan" { Generate-ExchangePlanDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
"ChartOfAccounts" { Generate-ChartOfAccountsDSL -meta $meta -presetData $presetData -purpose $effectivePurpose }
}
# Emit DSL if requested
if ($EmitDsl) {
$dslJson = $dsl | ConvertTo-Json -Depth 20
$dslPath = if ([System.IO.Path]::IsPathRooted($EmitDsl)) { $EmitDsl } else { Join-Path (Get-Location) $EmitDsl }
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($dslPath, $dslJson, $enc)
Write-Host "[from-object] DSL saved: $dslPath"
}
# Feed DSL into existing compiler
$dslJson = $dsl | ConvertTo-Json -Depth 20
$def = $dslJson | ConvertFrom-Json
} else {
# --- 1. Load and validate JSON (original mode) ---
if (-not (Test-Path $JsonPath)) {
Write-Error "File not found: $JsonPath"
exit 1
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
}
# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile)
$script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path }
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
}
# --- 2. ID allocator ---
$script:nextId = 1
function New-Id {
$id = $script:nextId
$script:nextId++
return $id
}
# Уникальность имён внутри коллекции (1С: элементы/реквизиты/команды/параметры/колонки — каждое своё
# пространство имён). Дубль → битый XML, форма не открывается, поэтому fail-fast.
function Assert-UniqueName {
param([string]$name, [hashtable]$seen, [string]$kind)
if ($seen.ContainsKey($name)) {
Write-Error "Duplicate $kind name '$name' — names must be unique within their collection in a 1C form (set a unique 'name')"
exit 1
}
$seen[$name] = $true
}
# --- 3. XML helper ---
$script:xml = New-Object System.Text.StringBuilder 8192
function X {
param([string]$text)
$script:xml.AppendLine($text) | Out-Null
}
function Esc-Xml {
# Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > .
# Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально);
# &quot; ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов.
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
# --- 4. Multilang helper ---
# Эмитит <v8:item> для значения: строка → один ru-элемент; объект {lang:text} → по элементу на язык.
function Emit-MLItems {
param($val, [string]$indent)
if ($val -is [System.Collections.IDictionary]) {
foreach ($k in $val.Keys) {
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
}
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
foreach ($p in $val.PSObject.Properties) {
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
}
} else {
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>"
}
}
function Emit-MLText {
param([string]$tag, $text, [string]$indent, [string]$xsiType)
$attr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" }
X "$indent<$tag$attr>"
Emit-MLItems -val $text -indent "$indent`t"
X "$indent</$tag>"
}
# <dcsset:userSettingPresentation> и подобные DCS-подписи: платформа пишет плоскую строку как
# xsi:type="xs:string" (скаляр; корпус 26), мультиязычный текст — как xsi:type="v8:LocalStringType"
# (7). Декомпилятор различает (Get-PresText: строка ИЛИ объект {ru,en}).
function Emit-USPresentation {
param($val, [string]$tag, [string]$indent)
if ($null -eq $val) { return }
if ($val -is [string]) {
X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)</$tag>"
} else {
Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType"
}
}
# Детектор «настоящей» inline-разметки форматированного текста (1С: <link>/<b>/<color>/…
# и закрывающий </>). Плейсхолдеры вида <не заполнен> НЕ срабатывают (нет известного тега/</>).
# ВАЖНО: regex должен быть идентичен в form-decompile (иначе гибрид-раундтрип поедет).
$script:fmtMarkupRe = '</>|<\s*(?:link|b|i|u|s|color|colorStyle|bgColor|bgColorStyle|font|fontSize|fontStyle|img)(?:\s|>)'
function Test-HasRealMarkup {
param($text)
if ($null -eq $text) { return $false }
$vals = if ($text -is [System.Collections.IDictionary]) { @($text.Values) }
elseif ($text -is [System.Management.Automation.PSCustomObject]) { @($text.PSObject.Properties.Value) }
else { @("$text") }
foreach ($v in $vals) { if ("$v" -match $script:fmtMarkupRe) { return $true } }
return $false
}
# DSL-значение ML-поля → @{ text; formatted }. Форма {text, formatted} = явный override;
# строка/мапа → авто-детект formatted по разметке.
function Resolve-MLFormatted {
param($val)
$hasText = $false
if ($val -is [System.Management.Automation.PSCustomObject]) { $hasText = [bool]$val.PSObject.Properties['text'] }
elseif ($val -is [System.Collections.IDictionary]) { $hasText = $val.Contains('text') }
if ($hasText) {
$t = if ($val -is [System.Collections.IDictionary]) { $val['text'] } else { $val.text }
$f = if ($val -is [System.Collections.IDictionary]) { $val['formatted'] } else { $val.formatted }
return @{ text = $t; formatted = [bool]$f }
}
return @{ text = $val; formatted = (Test-HasRealMarkup $val) }
}
# Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм).
# Декомпилятор опускает пустые настройки → компилятор регенерит этот скелет → раундтрип
# (harness нормализует GUID для хвоста с иными идентификаторами).
$script:CANON_FILTER_ID = 'dfcece9d-5077-440b-b6b3-45a5cb4538eb'
$script:CANON_ORDER_ID = '88619765-ccb3-46c6-ac52-38e9c992ebd4'
$script:CANON_CA_ID = 'b75fecce-942b-4aed-abc9-e6a02e460fb3'
$script:CANON_ITEMS_ID = '911b6018-f537-43e8-a417-da56b22f9aec'
# ─────────────────────────────────────────────────────────────────────────────
# Настройки компоновщика ListSettings: filter/order/conditionalAppearance.
# Грамматика DSL и эмиссия dcsset скопированы из skd-compile (навыки автономны).
# ─────────────────────────────────────────────────────────────────────────────
function New-Guid-String { return [System.Guid]::NewGuid().ToString() }
$script:comparisonTypes = @{
"=" = "Equal"; "<>" = "NotEqual"
">" = "Greater"; ">=" = "GreaterOrEqual"
"<" = "Less"; "<=" = "LessOrEqual"
"in" = "InList"; "notIn" = "NotInList"
"inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy"
"contains" = "Contains"; "notContains" = "NotContains"
"beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith"
"like" = "Like"; "notLike" = "NotLike"
"подобно" = "Like"; "неподобно" = "NotLike" # рус. синоним (хэш регистронезависим: ПОДОБНО=подобно)
"filled" = "Filled"; "notFilled" = "NotFilled"
}
function Parse-FilterShorthand {
param([string]$s)
$result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null; presentation = $null }
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()
$opPatterns = @('<>', '>=', '<=', '=', '>', '<',
'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b',
'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b',
'notLike\b', 'like\b', 'неподобно\b', 'подобно\b',
'notFilled\b', 'filled\b')
$opJoined = $opPatterns -join '|'
if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") {
$result.field = $Matches[1].Trim()
$result.op = $Matches[2].Trim()
$valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" }
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 } # дата без valueType → Emit-FilterItem выведет StandardBeginningDate Custom (дефолт даты в фильтре)
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 { $result.field = $s }
return $result
}
# Значение типа v8:Type (напр. тип «Неопределено» = <prefix>:Undefined) ссылается на тип
# платформы из namespace http://v8.1c.ru/8.2/data/types — платформа объявляет его ЛОКАЛЬНО
# на теге значения (префикс авто-назначаемый: d6p1/d8p1/dN…). Без объявления QName битый.
# Возвращает строку-атрибут ' xmlns:<pref>="…"' (перед xsi:type), либо "".
function Get-ValueTypeNsAttr {
param([string]$valueType, [string]$value)
if ($valueType -eq 'v8:Type' -and "$value" -match '^([A-Za-z]\w*):') {
$pref = $Matches[1]
if ($pref -notin @('xs','cfg','v8','v8ui','ent','dcscor','dcsset','dcssch')) {
return " xmlns:$pref=`"http://v8.1c.ru/8.2/data/types`""
}
}
return ""
}
function Emit-FilterItem {
param($item, [string]$indent)
if ($item.group) {
$groupType = switch ("$($item.group)") { "And" { "AndGroup" } "Or" { "OrGroup" } "Not" { "NotGroup" } default { "$($item.group)Group" } }
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemGroup`">"
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" } # группа отключена (перед groupType, порядок исходника)
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"
}
}
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
if ($item.userSettingID) {
$guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>"
}
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:item>"
return
}
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>"
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
if ($valIsArray) {
if (@($item.value).Count -eq 0) {
X "$indent`t<dcsset:right xsi:type=`"v8:ValueListType`">"
X "$indent`t`t<v8:valueType/>"
X "$indent`t`t<v8:lastId xsi:type=`"xs:decimal`">-1</v8:lastId>"
X "$indent`t</dcsset:right>"
} else {
foreach ($v in $item.value) {
$vt = if ($item.valueType) { "$($item.valueType)" } else { "" }
if (-not $vt) {
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' }
elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = 'xs:decimal' }
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
else { $vt = 'xs:string' }
}
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" }
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v"
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
}
}
} elseif ($null -ne $item.value -and (
"$($item.valueType)" -match 'Standard(Beginning|End)Date$' -or
(-not $item.valueType -and "$($item.value)" -match '^\d{4}-\d{2}-\d{2}T'))) {
# Стандартная дата начала/окончания. Формы значения:
# объект {variant, date?} — полная (Custom несёт <v8:date>);
# строка-вариант "BeginningOfThisDay" — именованный вариант без даты;
# голая ISO-дата без valueType — шорткат для Custom+date (дата в фильтре платформой
# почти всегда хранится как StandardBeginningDate Custom, корпус 268 vs 2 xs:dateTime;
# явный valueType="xs:dateTime" → плоская дата, ветка ниже).
$sdType = if ($item.valueType) { "$($item.valueType)" -replace '^v8:','' } else { 'StandardBeginningDate' }
$sv = $item.value
if (($sv -is [PSCustomObject]) -or ($sv -is [System.Collections.IDictionary])) {
$variant = if ($sv -is [PSCustomObject]) { "$($sv.variant)" } else { "$($sv['variant'])" }
$hasDate = if ($sv -is [PSCustomObject]) { [bool]$sv.PSObject.Properties['date'] } else { $sv.Contains('date') }
$dateV = if ($hasDate) { if ($sv -is [PSCustomObject]) { "$($sv.date)" } else { "$($sv['date'])" } } else { $null }
} elseif ("$sv" -match '^\d{4}-\d{2}-\d{2}T') {
$variant = 'Custom'; $hasDate = $true; $dateV = "$sv"
} else {
$variant = "$sv"; $hasDate = $false; $dateV = $null
}
X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">"
X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-Xml $variant)</v8:variant>"
if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-Xml $dateV)</v8:date>" }
X "$indent`t</dcsset:right>"
} elseif ("$($item.value)" -eq '_') {
# "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right>
# (напр. <dcsset:right xsi:type="dcscor:Field"/> — сравнение с незаданным полем).
$vt = if ($item.valueType) { "$($item.valueType)" } else { 'xs:string' }
X "$indent`t<dcsset:right xsi:type=`"$vt`"/>"
} elseif ($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" }
elseif ("$v" -match '^-?\d+(\.\d+)?$') { $vt = "xs:decimal" }
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
else { $vt = "xs:string" }
}
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)"
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
}
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
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) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:item>"
}
function Emit-Filter {
param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null)
$hasItems = $items -and $items.Count -gt 0
$hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation)
if (-not $hasItems -and -not $hasBlockMeta) { return }
X "$indent<dcsset:filter>"
foreach ($item in $items) {
if ($item -is [string]) {
$parsed = Parse-FilterShorthand $item
$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 }
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
} else { Emit-FilterItem -item $item -indent "$indent`t" }
}
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
if ($null -ne $blockUserSettingID) {
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
}
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:filter>"
}
function Emit-Order {
param($items, [string]$indent, [switch]$skipAuto, $blockViewMode = $null, $blockUserSettingID = $null, $blockUserSettingPresentation = $null)
$hasItems = $items -and $items.Count -gt 0
$hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation)
if (-not $hasItems -and -not $hasBlockMeta) { 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>"
}
} else {
if ($item.field -eq "Auto" -or $item.type -eq "auto") { if (-not $skipAuto) { X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemAuto`"/>" }; continue }
$dir = if ($item.direction) { "$($item.direction)" } else { "Asc" }
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>"
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
X "$indent`t</dcsset:item>"
}
}
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
if ($null -ne $blockUserSettingID) {
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
}
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</dcsset:order>"
}
function Emit-AppearanceValue {
param([string]$key, $val, [string]$indent)
X "$indent<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
function _HasKey { param($o, [string]$k)
if ($o -is [PSCustomObject]) { return [bool]$o.PSObject.Properties[$k] }
if ($o -is [System.Collections.IDictionary]) { return $o.Contains($k) }
return $false
}
function _Get { param($o, [string]$k)
if ($o -is [PSCustomObject]) { return $o.$k }
if ($o -is [System.Collections.IDictionary]) { return $o[$k] }
return $null
}
$isTopLevelLine = (_HasKey $val '@type') -and ("$(_Get $val '@type')" -eq 'Line')
$useWrapper = $false
$innerVal = $val
$nestedItems = $null
if ($isTopLevelLine) {
if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true }
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
} elseif ((_HasKey $val 'value') -and (($val -is [PSCustomObject]) -or ($val -is [System.Collections.IDictionary]))) {
$innerVal = (_Get $val 'value')
if ((_HasKey $val 'use') -and ((_Get $val 'use') -eq $false)) { $useWrapper = $true }
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
}
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
$isFontDict = $false
if ($innerVal -is [PSCustomObject]) {
$tProp = $innerVal.PSObject.Properties['@type']
if ($tProp -and "$($tProp.Value)" -eq 'Font') { $isFontDict = $true }
} elseif ($innerVal -is [System.Collections.IDictionary]) {
if ($innerVal.Contains('@type') -and "$($innerVal['@type'])" -eq 'Font') { $isFontDict = $true }
}
$isLineDict = $false
if (_HasKey $innerVal '@type') { $isLineDict = ("$(_Get $innerVal '@type')" -eq 'Line') }
$isDict = ($innerVal -is [hashtable]) -or ($innerVal -is [System.Collections.IDictionary]) -or ($innerVal -is [PSCustomObject])
if ($isLineDict) {
$lw = if (_HasKey $innerVal 'width') { _Get $innerVal 'width' } else { 0 }
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>"
X "$indent`t</dcscor:value>"
} elseif ($isFontDict) {
$attrParts = @()
foreach ($attrName in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
$av = $null
if ($innerVal -is [PSCustomObject]) { $ap = $innerVal.PSObject.Properties[$attrName]; if ($ap) { $av = $ap.Value } }
else { if ($innerVal.Contains($attrName)) { $av = $innerVal[$attrName] } }
if ($null -ne $av) { $attrParts += "$attrName=`"$(Esc-Xml "$av")`"" }
}
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
} elseif ($isDict -and (_HasKey $innerVal 'field')) {
# Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-Xml "$(_Get $innerVal 'field')")</dcscor:value>"
} elseif ($isDict) {
# Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType"
} else {
$actualVal = "$innerVal"
$keyTypeMap = @{
'Размещение' = 'dcscor:DataCompositionTextPlacementType'
'ГоризонтальноеПоложение' = 'v8ui:HorizontalAlign'
'ВертикальноеПоложение' = 'v8ui:VerticalAlign'
'ОриентацияТекста' = 'xs:decimal'
'РасположениеИтогов' = 'dcscor:DataCompositionTotalPlacement'
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
}
$keyType = $keyTypeMap[$key]
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" }
elseif ($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 "Формат") {
# Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит
# нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше).
# Пустая строка → самозакрывающийся тег (как у платформы).
if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" }
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
}
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
}
if ($nestedItems) {
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
if ($niProps) { foreach ($np in $niProps) { Emit-AppearanceValue -key $np.Name -val $np.Value -indent "$indent`t" } }
elseif ($nestedItems -is [System.Collections.IDictionary]) { foreach ($nk in $nestedItems.Keys) { Emit-AppearanceValue -key $nk -val $nestedItems[$nk] -indent "$indent`t" } }
}
X "$indent</dcscor:item>"
}
function Emit-ConditionalAppearance {
param($items, [string]$indent, $blockViewMode = $null, $blockUserSettingID = $null, [string]$wrapTag = 'dcsset:conditionalAppearance', $blockUserSettingPresentation = $null)
$hasItems = $items -and $items.Count -gt 0
$hasBlockMeta = ($null -ne $blockViewMode) -or ($null -ne $blockUserSettingID) -or ($null -ne $blockUserSettingPresentation)
if (-not $hasItems -and -not $hasBlockMeta) { return }
X "$indent<$wrapTag>"
foreach ($ca in $items) {
X "$indent`t<dcsset:item>"
if ($ca.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
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/>" }
if ($ca.filter -and $ca.filter.Count -gt 0) { Emit-Filter -items $ca.filter -indent "$indent`t`t" }
else { X "$indent`t`t<dcsset:filter/>" }
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>"
}
if ($ca.presentation) {
if ($ca.presentation -is [hashtable] -or $ca.presentation -is [System.Collections.IDictionary] -or $ca.presentation -is [PSCustomObject]) {
# Мультиязык → LocalStringType (платформа объявляет тип у локализованного presentation)
X "$indent`t`t<dcsset:presentation xsi:type=`"v8:LocalStringType`">"
Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t"
X "$indent`t`t</dcsset:presentation>"
}
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" }
}
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" }
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>"
}
if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
$useInOrder = @('group','hierarchicalGroup','overall','fieldsHeader','header','parameters','filter','resourceFieldsHeader','overallHeader','overallResourceFieldsHeader')
$set = @{}
foreach ($n in $ca.useInDontUse) { $set["$n"] = $true }
foreach ($n in $useInOrder) {
if ($set.ContainsKey($n)) {
$tag = "useIn" + ($n.Substring(0,1).ToUpper()) + ($n.Substring(1))
X "$indent`t`t<dcsset:$tag>DontUse</dcsset:$tag>"
}
}
}
X "$indent`t</dcsset:item>"
}
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
if ($null -ne $blockUserSettingID) {
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
}
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
X "$indent</$wrapTag>"
}
# === Группировка строк динамического списка (DCS-структура ListSettings) ===
# Линейная цепочка <dcsset:item StructureItemGroup> (каждый уровень = одно поле в groupItems;
# вложенность — через дочерний <dcsset:item>). Зеркало skd Emit-GroupItems/Emit-StructureItem,
# но плоская модель уровней (список всегда линеен, без selection/order/children).
function Get-ListGroupingValue {
param($st)
foreach ($k in 'grouping','structure','группировка') {
if ($st.PSObject.Properties[$k] -and $st.$k) { return $st.$k }
}
return $null
}
function Parse-ListGrouping {
param($grouping)
# Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть.
# Unary comma: иначе PS разворачивает одноэлементный массив при return → строка → индексация даёт char.
if (-not $grouping) { return ,@() }
if ($grouping -is [string]) { return ,@($grouping -split '\s*>\s*' | Where-Object { "$_" -ne '' }) }
return ,@($grouping)
}
function Emit-GroupItemField {
param($level, [string]$indent)
if ($level -is [string]) {
$field = $level; $gt = 'Items'; $pat = 'None'; $pab = '0001-01-01T00:00:00'; $pae = '0001-01-01T00:00:00'
} else {
$field = "$($level.field)"
$gt = if ($level.groupType) { "$($level.groupType)" } else { 'Items' }
$pat = if ($level.periodAdditionType) { "$($level.periodAdditionType)" } else { 'None' }
$pab = if ($level.periodAdditionBegin) { "$($level.periodAdditionBegin)" } else { '0001-01-01T00:00:00' }
$pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' }
}
X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
X "$indent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
X "$indent`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>"
X "$indent`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>"
# Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
$pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
$paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>"
X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>"
X "$indent</dcsset:item>"
}
function Emit-ListGroupingLevels {
param($levels, [int]$i, [string]$indent)
X "$indent<dcsset:item xsi:type=`"dcsset:StructureItemGroup`">"
X "$indent`t<dcsset:groupItems>"
Emit-GroupItemField $levels[$i] "$indent`t`t"
X "$indent`t</dcsset:groupItems>"
if ($i -lt $levels.Count - 1) { Emit-ListGroupingLevels $levels ($i + 1) "$indent`t" }
X "$indent</dcsset:item>"
}
function Emit-ListGrouping {
param($grouping, [string]$indent)
$levels = Parse-ListGrouping $grouping
if ($levels.Count -eq 0) { return }
Emit-ListGroupingLevels $levels 0 $indent
}
# === Вычисляемые поля DataSet динамического списка (<CalculatedField>) ===
# Зеркало skd: shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder"
# или объект. Форм-специфика: dcssch:-теги + presentationExpression/orderExpression (dcscommon ns).
$script:calcRestrictMap = @{ 'noField'='field'; 'noFilter'='condition'; 'noCondition'='condition'; 'noGroup'='group'; 'noOrder'='order' }
$script:dcsCommonNs = 'http://v8.1c.ru/8.1/data-composition-system/common'
function Parse-CalcShorthand {
param([string]$s)
$restrict = @()
foreach ($m in [regex]::Matches($s, '#(noField|noFilter|noCondition|noGroup|noOrder)\b')) { $restrict += $m.Groups[1].Value }
$s = [regex]::Replace($s, '\s*#(noField|noFilter|noCondition|noGroup|noOrder)\b', '')
$eq = $s.IndexOf('=')
if ($eq -gt 0) { $lhs = $s.Substring(0, $eq); $rhs = $s.Substring($eq + 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 }
}
function Emit-CalcFields {
param($calcFields, [string]$indent)
if (-not $calcFields) { return }
foreach ($cf in $calcFields) {
$pres = $null; $orderExpr = $null; $restrict = @()
if ($cf -is [string]) {
$p = Parse-CalcShorthand $cf
$dataPath = "$($p.dataPath)"; $expression = "$($p.expression)"; $title = $p.title; $typeStr = "$($p.type)"
foreach ($r in $p.restrict) { if ($script:calcRestrictMap[$r]) { $restrict += $script:calcRestrictMap[$r] } }
} else {
$dataPath = if ($cf.dataPath) { "$($cf.dataPath)" } elseif ($cf.field) { "$($cf.field)" } else { "$($cf.name)" }
$expression = "$($cf.expression)"
$title = $cf.title
$typeStr = if ($cf.valueType) { "$($cf.valueType)" } elseif ($cf.type) { "$($cf.type)" } else { '' }
$ur = if ($cf.useRestriction) { $cf.useRestriction } elseif ($cf.restrict) { $cf.restrict } else { $null }
if ($ur -is [System.Management.Automation.PSCustomObject] -or $ur -is [hashtable]) {
foreach ($k in 'field','condition','group','order') { if ($ur.$k -eq $true) { $restrict += $k } }
} elseif ($ur -is [string]) {
foreach ($tok in ($ur -split '\s+')) { $t = $tok.Trim().TrimStart('#'); if ($t) { $restrict += $(if ($script:calcRestrictMap[$t]) { $script:calcRestrictMap[$t] } else { $t }) } }
} elseif ($ur) {
foreach ($r in $ur) { $rr = "$r"; $restrict += $(if ($script:calcRestrictMap[$rr]) { $script:calcRestrictMap[$rr] } else { $rr }) }
}
$pres = $cf.presentationExpression
$orderExpr = $cf.orderExpression
}
$ci = "$indent`t"
X "$indent<CalculatedField>"
X "$ci<dcssch:dataPath>$(Esc-Xml $dataPath)</dcssch:dataPath>"
X "$ci<dcssch:expression>$(Esc-Xml $expression)</dcssch:expression>"
if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' }
if ($restrict.Count -gt 0) {
X "$ci<dcssch:useRestriction>"
foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } }
X "$ci</dcssch:useRestriction>"
}
if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-Xml "$pres")</dcssch:presentationExpression>" }
if ($orderExpr) {
$oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) }
foreach ($oe in $oeList) {
if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' }
else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } }
X "$ci<dcssch:orderExpression>"
X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-Xml $exprV)</expression>"
X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>"
X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>"
X "$ci</dcssch:orderExpression>"
}
}
if ($typeStr) { Emit-DLValueType -typeStr $typeStr -indent $ci }
X "$indent</CalculatedField>"
}
}
# Ограничения использования поля/вычисляемого поля (useRestriction / attributeUseRestriction).
# Значение: объект {field?,condition?,group?,order?} | флаг-строка "#noField #noFilter #noGroup #noOrder" | массив.
function Get-RestrictList {
param($ur)
$out = @()
if (-not $ur) { return ,$out }
if ($ur -is [System.Management.Automation.PSCustomObject] -or $ur -is [hashtable]) {
foreach ($k in 'field','condition','group','order') { if ($ur.$k -eq $true) { $out += $k } }
} elseif ($ur -is [string]) {
foreach ($tok in ($ur -split '\s+')) { $t = $tok.Trim().TrimStart('#'); if ($t) { $out += $(if ($script:calcRestrictMap[$t]) { $script:calcRestrictMap[$t] } else { $t }) } }
} else {
foreach ($r in $ur) { $rr = "$r"; $out += $(if ($script:calcRestrictMap[$rr]) { $script:calcRestrictMap[$rr] } else { $rr }) }
}
return ,$out
}
function Emit-RestrictBlock {
param([string]$tag, $ur, [string]$indent)
$r = Get-RestrictList $ur
if ($r.Count -eq 0) { return }
X "$indent<dcssch:$tag>"
foreach ($k in @('field','condition','group','order')) { if ($r -contains $k) { X "$indent`t<dcssch:$k>true</dcssch:$k>" } }
X "$indent</dcssch:$tag>"
}
# --- 5. Type emitter ---
$script:formTypeSynonyms = New-Object System.Collections.Hashtable
$script:formTypeSynonyms["строка"] = "string"
$script:formTypeSynonyms["число"] = "decimal"
$script:formTypeSynonyms["булево"] = "boolean"
$script:formTypeSynonyms["дата"] = "date"
$script:formTypeSynonyms["датавремя"]= "dateTime"
$script:formTypeSynonyms["number"] = "decimal"
$script:formTypeSynonyms["bool"] = "boolean"
$script:formTypeSynonyms["справочникссылка"] = "CatalogRef"
$script:formTypeSynonyms["справочникобъект"] = "CatalogObject"
$script:formTypeSynonyms["документссылка"] = "DocumentRef"
$script:formTypeSynonyms["документобъект"] = "DocumentObject"
$script:formTypeSynonyms["перечислениессылка"] = "EnumRef"
$script:formTypeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef"
$script:formTypeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef"
$script:formTypeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef"
$script:formTypeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef"
$script:formTypeSynonyms["планобменассылка"] = "ExchangePlanRef"
$script:formTypeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef"
$script:formTypeSynonyms["задачассылка"] = "TaskRef"
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
$script:formTypeSynonyms["характеристика"] = "Characteristic"
$script:formTypeSynonyms["любаяссылка"] = "AnyRef"
$script:formTypeSynonyms["любаяссылкаиб"] = "AnyIBRef"
# Платформенные v8-типы (forgiving: англ. без префикса + рус.) → каноничный с префиксом v8: (эмитим verbatim)
$script:formTypeSynonyms["standardperiod"] = "v8:StandardPeriod"
$script:formTypeSynonyms["стандартныйпериод"] = "v8:StandardPeriod"
$script:formTypeSynonyms["standardbeginningdate"] = "v8:StandardBeginningDate"
$script:formTypeSynonyms["стандартнаядатаначала"] = "v8:StandardBeginningDate"
$script:formTypeSynonyms["uuid"] = "v8:UUID"
$script:formTypeSynonyms["уникальныйидентификатор"] = "v8:UUID"
$script:formTypeSynonyms["списокзначений"] = "ValueList"
# Known invalid types (runtime/UI types that don't exist in XDTO schema)
$script:knownInvalidTypes = @{
"FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)"
"FormDataCollection" = "Runtime type. Use ValueTable"
"FormDataTree" = "Runtime type. Use ValueTree"
"FormDataTreeItem" = "Runtime type, not valid in XML"
"FormDataCollectionItem"= "Runtime type, not valid in XML"
"FormGroup" = "UI element type, not a data type"
"FormField" = "UI element type, not a data type"
"FormButton" = "UI element type, not a data type"
"FormDecoration" = "UI element type, not a data type"
"FormTable" = "UI element type, not a data type"
}
function Resolve-TypeStr {
param([string]$typeStr)
if (-not $typeStr) { return $typeStr }
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] }
if ($typeStr -match '^([^(]+)\((.+)\)$') {
$base = $Matches[1].Trim(); $params = $Matches[2]
$r = $script:formTypeSynonyms[$base.ToLower()]
if ($r) { return "$r($params)" }
return $typeStr
}
if ($typeStr.Contains('.')) {
$i = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
$r = $script:formTypeSynonyms[$prefix.ToLower()]
if ($r) { return "$r$suffix" }
return $typeStr
}
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
if ($r) { return $r }
return $typeStr
}
function Emit-Type {
# $tag/$tagAttrs — обёртка (по умолчанию <Type>); для уточнения типа значений ValueList
# вызывается с tag="Settings", tagAttrs=' xsi:type="v8:TypeDescription"'.
param($typeStr, [string]$indent, [string]$tag = "Type", [string]$tagAttrs = "")
if (-not $typeStr) {
X "$indent<$tag$tagAttrs/>"
return
}
$typeString = "$typeStr"
# Composite type: "Type1 | Type2" or "Type1 + Type2"
$parts = $typeString -split '\s*[|+]\s*'
X "$indent<$tag$tagAttrs>"
foreach ($part in $parts) {
$part = $part.Trim()
Emit-SingleType -typeStr $part -indent "$indent`t"
}
X "$indent</$tag>"
}
function Emit-SingleType {
param([string]$typeStr, [string]$indent)
$typeStr = Resolve-TypeStr $typeStr
# TypeId — тип, заданный глобальным стабильным GUID (<v8:TypeId>, не <v8:Type>). Платформа так
# сериализует типы, чьё имя в этом контексте недоступно (определяемые/характеристики). GUID
# глобально стабилен → эмитим verbatim (как роль-по-GUID). Маркер декомпилятора: 'typeid:GUID'.
if ($typeStr -match '^typeid:([0-9a-fA-F-]{36})$') {
X "$indent<v8:TypeId>$($Matches[1])</v8:TypeId>"
return
}
# boolean
if ($typeStr -eq "boolean") {
X "$indent<v8:Type>xs:boolean</v8:Type>"
return
}
# string or string(N) or string(N,fixed) (AllowedLength: Variable дефолт / Fixed)
if ($typeStr -match '^string(\((\d+)(\s*,\s*(fixed|variable))?\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "0" }
$al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' }
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>$al</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 / time
if ($typeStr -match '^(date|dateTime|time)$') {
$fractions = switch ($typeStr) {
"date" { "Date" }
"dateTime" { "DateTime" }
"time" { "Time" }
}
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
}
# ValueTable, ValueTree, ValueList, etc.
$v8Types = @{
"ValueTable" = "v8:ValueTable"
"ValueTree" = "v8:ValueTree"
"ValueList" = "v8:ValueListType"
"TypeDescription" = "v8:TypeDescription"
"Universal" = "v8:Universal"
"FixedArray" = "v8:FixedArray"
"FixedStructure" = "v8:FixedStructure"
}
if ($v8Types.ContainsKey($typeStr)) {
X "$indent<v8:Type>$($v8Types[$typeStr])</v8:Type>"
return
}
# UI types
$uiTypes = @{
"FormattedString" = "v8ui:FormattedString"
"Picture" = "v8ui:Picture"
"Color" = "v8ui:Color"
"Font" = "v8ui:Font"
}
if ($uiTypes.ContainsKey($typeStr)) {
X "$indent<v8:Type>$($uiTypes[$typeStr])</v8:Type>"
return
}
# DCS types
if ($typeStr -match '^DataComposition') {
$dcsMap = @{
"DataCompositionSettings" = "dcsset:DataCompositionSettings"
"DataCompositionSchema" = "dcssch:DataCompositionSchema"
"DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType"
}
if ($dcsMap.ContainsKey($typeStr)) {
X "$indent<v8:Type>$($dcsMap[$typeStr])</v8:Type>"
return
}
}
# Голые конфигурационные типы (cfg: без .Имя): дин-список, набор констант, общий объект отчёта.
# Корпус (acc+erp 8.3.24): DynamicList 5205, ConstantsSet 103, ReportObject 10. (Дотированные формы
# ConstantsSet.X / ReportObject.X ловит общий cfg:-regex ниже.)
if ($typeStr -in @("DynamicList","ConstantsSet","ReportObject")) {
X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
return
}
# TypeSet (набор типов) → <v8:TypeSet>: определяемый тип / характеристика (именованные)
# + «любая ссылка вида» (голый ref-вид без .Имя). Развязка с обычным типом — по наличию точки.
if ($typeStr -match '^(DefinedType|Characteristic)\.') {
X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>"
return
}
if ($typeStr -match '^(AnyRef|AnyIBRef|CatalogRef|DocumentRef|EnumRef|ExchangePlanRef|TaskRef|BusinessProcessRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef)$') {
X "$indent<v8:TypeSet>cfg:$typeStr</v8:TypeSet>"
return
}
# cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.)
if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfAccountsObject|ChartOfCharacteristicTypesRef|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesRef|ChartOfCalculationTypesObject|ExchangePlanRef|ExchangePlanObject|BusinessProcessRef|BusinessProcessObject|TaskRef|TaskObject|InformationRegisterRecordSet|InformationRegisterRecordManager|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|ConstantsSet|DataProcessorObject|ReportObject)\.') {
X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
return
}
# Спец-типы платформы с собственным namespace (объявляется ЛОКАЛЬНО на <v8:Type>).
# Префикс d5p1 неоднозначен (5 разных URI), поэтому маппинг по полному значению типа.
# К таким типам привязаны спец-поля: mxl→SpreadSheetDocumentField, fd→FormattedDocumentField,
# d5p1:TextDocument→TextDocumentField, pdfdoc→PDF, pl→Planner, chart/geo/graphscheme/data-analysis.
$specialTypeNs = @{
"mxl:SpreadsheetDocument" = "http://v8.1c.ru/8.2/data/spreadsheet"
"fd:FormattedDocument" = "http://v8.1c.ru/8.2/data/formatted-document"
"d5p1:TextDocument" = "http://v8.1c.ru/8.1/data/txtedt"
"d5p1:Chart" = "http://v8.1c.ru/8.2/data/chart"
"d5p1:GanttChart" = "http://v8.1c.ru/8.2/data/chart"
"d5p1:Dendrogram" = "http://v8.1c.ru/8.2/data/chart"
"d5p1:FlowchartContextType" = "http://v8.1c.ru/8.2/data/graphscheme"
"d5p1:DataAnalysisTimeIntervalUnitType" = "http://v8.1c.ru/8.2/data/data-analysis"
"d5p1:GeographicalSchema" = "http://v8.1c.ru/8.2/data/geo"
"pdfdoc:PDFDocument" = "http://v8.1c.ru/8.3/data/pdf"
"pl:Planner" = "http://v8.1c.ru/8.3/data/planner"
}
if ($specialTypeNs.ContainsKey($typeStr)) {
$pref = $typeStr.Substring(0, $typeStr.IndexOf(':'))
X "$indent<v8:Type xmlns:$pref=`"$($specialTypeNs[$typeStr])`">$typeStr</v8:Type>"
return
}
# Fallback with validation
if ($script:knownInvalidTypes.ContainsKey($typeStr)) {
throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])"
}
# Платформенный тип с префиксом (v8:/v8ui:/xs:/dcs*:) — эмитим verbatim (напр. v8:UUID, v8:StandardPeriod).
if ($typeStr -match '^(v8|v8ui|xs|ent|style|sys|web|win|dcs\w*):') {
X "$indent<v8:Type>$typeStr</v8:Type>"
} elseif ($typeStr.Contains('.')) {
X "$indent<v8:Type>cfg:$typeStr</v8:Type>"
} else {
Write-Warning "Unrecognized bare type '$typeStr' — will be emitted without namespace prefix"
X "$indent<v8:Type>$typeStr</v8:Type>"
}
}
# --- 6. Event handler name generator ---
$script:eventSuffixMap = @{
"OnChange" = "ПриИзменении"
"StartChoice" = "НачалоВыбора"
"ChoiceProcessing" = "ОбработкаВыбора"
"AutoComplete" = "АвтоПодбор"
"Clearing" = "Очистка"
"Opening" = "Открытие"
"Click" = "Нажатие"
"OnActivateRow" = "ПриАктивизацииСтроки"
"BeforeAddRow" = "ПередНачаломДобавления"
"BeforeDeleteRow" = "ПередУдалением"
"BeforeRowChange" = "ПередНачаломИзменения"
"OnStartEdit" = "ПриНачалеРедактирования"
"OnEndEdit" = "ПриОкончанииРедактирования"
"Selection" = "ВыборСтроки"
"OnCurrentPageChange" = "ПриСменеСтраницы"
"TextEditEnd" = "ОкончаниеВводаТекста"
"URLProcessing" = "ОбработкаНавигационнойСсылки"
"DragStart" = "НачалоПеретаскивания"
"Drag" = "Перетаскивание"
"DragCheck" = "ПроверкаПеретаскивания"
"Drop" = "Помещение"
"AfterDeleteRow" = "ПослеУдаления"
}
function Get-HandlerName {
param([string]$elementName, [string]$eventName)
$suffix = $script:eventSuffixMap[$eventName]
if ($suffix) {
return "$elementName$suffix"
}
return "$elementName$eventName"
}
# --- 7. Element emitters ---
function Get-ElementName {
param($el, [string]$typeKey)
if ($el.name) { return "$($el.name)" }
return "$($el.$typeKey)"
}
$script:knownEvents = @{
"input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
"check" = @("OnChange")
"radio" = @("OnChange")
"label" = @("Click","URLProcessing")
"labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing")
"table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange")
"pages" = @("OnCurrentPageChange")
"page" = @("OnCurrentPageChange")
"button" = @("Click")
"picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing")
"calendar" = @("OnChange","OnActivate")
"picture" = @("Click")
"cmdBar" = @()
"popup" = @()
"group" = @()
}
$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening")
# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL.
# Основной формат: $el.events = { Событие: ИмяОбработчика } (null/"" → авто-имя по конвенции).
# Legacy (принимается ради совместимости): $el.on (массив) + $el.handlers (переопределение имён).
function Get-EventPairs {
param($el, [string]$elementName)
$pairs = New-Object System.Collections.ArrayList
if ($el.events) {
foreach ($p in $el.events.PSObject.Properties) {
$h = "$($p.Value)"
if ([string]::IsNullOrEmpty($h)) { $h = Get-HandlerName -elementName $elementName -eventName $p.Name }
[void]$pairs.Add([pscustomobject]@{ name = $p.Name; handler = $h })
}
} elseif ($el.on) {
foreach ($evt in $el.on) {
$evtName = "$evt"
$h = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } else { Get-HandlerName -elementName $elementName -eventName $evtName }
[void]$pairs.Add([pscustomobject]@{ name = $evtName; handler = $h })
}
}
return $pairs
}
# Проверить, подключено ли событие к элементу (в любом из форматов).
function Test-ElementEvent {
param($el, [string]$eventName)
if ($el.events) {
foreach ($p in $el.events.PSObject.Properties) { if ($p.Name -eq $eventName) { return $true } }
}
if ($el.on -contains $eventName) { return $true }
return $false
}
function Emit-Events {
param($el, [string]$elementName, [string]$indent, [string]$typeKey)
$pairs = Get-EventPairs -el $el -elementName $elementName
if ($pairs.Count -eq 0) { return }
# Validate event names
if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) {
$allowed = $script:knownEvents[$typeKey]
foreach ($pr in $pairs) {
if ($allowed.Count -gt 0 -and $allowed -notcontains "$($pr.name)") {
Write-Host "[WARN] Unknown event '$($pr.name)' for $typeKey '$elementName'. Known: $($allowed -join ', ')"
}
}
}
X "$indent<Events>"
foreach ($pr in $pairs) {
X "$indent`t<Event name=`"$($pr.name)`">$($pr.handler)</Event>"
}
X "$indent</Events>"
}
# ExtendedTooltip — это LabelDecoration: может нести own-content (layout/оформление/флаги/hyperlink)
# вместо/вместе с текстом. Признак структурированной формы: объект с любым НЕ-текстовым ключом
# ({text,formatted} и языковые ключи {ru,en} → обычная текст-форма).
$script:companionStructKeys = @(
'width','autoMaxWidth','maxWidth','height','autoMaxHeight','maxHeight','verticalAlign','titleHeight',
'horizontalStretch','verticalStretch','horizontalAlign','groupHorizontalAlign','groupVerticalAlign',
'visible','hidden','enabled','disabled','hyperlink','events','tooltip',
'textColor','backColor','borderColor','font','border','цветтекста','цветфона','цветрамки','шрифт','рамка'
)
function Test-CompanionStructured {
param($content)
if (-not (($content -is [System.Collections.IDictionary]) -or ($content -is [System.Management.Automation.PSCustomObject]))) { return $false }
foreach ($k in $script:companionStructKeys) {
$present = if ($content -is [System.Collections.IDictionary]) { $content.Contains($k) } else { [bool]$content.PSObject.Properties[$k] }
if ($present) { return $true }
}
return $false
}
function Emit-CompanionTitle {
param($content, [string]$indent)
$r = Resolve-MLFormatted $content
$fmt = if ($r.formatted) { 'true' } else { 'false' }
X "$indent<Title formatted=`"$fmt`">"
Emit-MLItems -val $r.text -indent "$indent`t"
X "$indent</Title>"
}
# DisplayImportance — атрибут открывающего тега элемента (адаптивная важность: VeryHigh/High/Usual/Low/VeryLow).
# Возвращает ` DisplayImportance="X"` или "" (для companion-эмиттеров без $el → "" молча).
function DI-Attr {
param($el)
if ($null -ne $el -and $el.displayImportance) { return " DisplayImportance=`"$(Esc-Xml "$($el.displayImportance)")`"" }
return ""
}
function Emit-Companion {
param([string]$tag, [string]$name, [string]$indent, $content = $null)
$id = New-Id
$hasContent = $null -ne $content -and -not ($content -is [string] -and "$content" -eq '')
if (-not $hasContent) {
X "$indent<$tag name=`"$name`" id=`"$id`"/>"
return
}
$inner = "$indent`t"
# DI-Attr берём от СОБСТВЕННОГО объекта компаньона ($content), НЕ от ambient $el родителя
# (PowerShell dynamic scope — иначе companion наследует DisplayImportance владельца: баг).
X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $content)>"
if (Test-CompanionStructured $content) {
# структурированная форма (own-content). Порядок как у платформы: own-content (флаги/hyperlink/
# layout/оформление) ПЕРЕД Title (в корпусе layout-first 582 vs 10).
$txtPresent = if ($content -is [System.Collections.IDictionary]) { $content.Contains('text') } else { [bool]$content.PSObject.Properties['text'] }
Emit-CommonFlags -el $content -indent $inner
if ($content.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
Emit-Layout -el $content -indent $inner
Emit-Appearance -el $content -indent $inner -profile 'decoration'
if ($txtPresent) { Emit-CompanionTitle -content $content -indent $inner }
# ToolTip компаньона (подсказка самой расширенной подсказки) — после Title (порядок схемы LabelDecoration)
if ($content.tooltip) { Emit-MLText -tag "ToolTip" -text $content.tooltip -indent $inner }
# События компаньона (ExtendedTooltip = LabelDecoration: напр. URLProcessing у hyperlink-подсказки)
Emit-Events -el $content -elementName $name -indent $inner -typeKey 'label'
} else {
Emit-CompanionTitle -content $content -indent $inner
}
X "$indent</$tag>"
}
# Companion-командная-панель (ContextMenu/AutoCommandBar) с контентом: { autofill?, children?[] }
# или массив = shorthand для { children }. Пусто/нет → self-closing companion (как Emit-Companion).
# Дети — обычная грамматика button/buttonGroup/popup (Emit-Element, inCmdBar).
function Emit-CompanionPanel {
param([string]$tag, [string]$name, [string]$indent, $panel)
$id = New-Id
$autofill = $null
$children = $null
$halign = $null
if ($panel -is [array]) {
$children = $panel
} elseif ($null -ne $panel) {
if ($null -ne $panel.PSObject.Properties['autofill'] -and $null -ne $panel.autofill) { $autofill = [bool]$panel.autofill }
if ($null -ne $panel.PSObject.Properties['horizontalAlign'] -and "$($panel.horizontalAlign)" -ne '') { $halign = "$($panel.horizontalAlign)" }
$children = $panel.children
}
$hasChildren = $children -and @($children).Count -gt 0
# Платформа пишет <Autofill> только при false; true = дефолт (тег опускается).
$emitAfFalse = ($autofill -eq $false)
if (-not $emitAfFalse -and -not $hasChildren -and -not $halign) {
X "$indent<$tag name=`"$name`" id=`"$id`"/>"
return
}
X "$indent<$tag name=`"$name`" id=`"$id`"$(DI-Attr $panel)>"
if ($halign) { X "$indent`t<HorizontalAlign>$halign</HorizontalAlign>" }
if ($emitAfFalse) { X "$indent`t<Autofill>false</Autofill>" }
if ($hasChildren) {
X "$indent`t<ChildItems>"
foreach ($c in @($children)) { Emit-Element -el $c -indent "$indent`t`t" -inCmdBar $true }
X "$indent`t</ChildItems>"
}
X "$indent</$tag>"
}
# Дополнения командной панели таблицы: тип DSL → XML-тег + AdditionSource.Type.
$script:additionTypeMap = [ordered]@{
'searchString' = @{ Tag = 'SearchStringAddition'; Type = 'SearchStringRepresentation'; Suffix = 'СтрокаПоиска' }
'viewStatus' = @{ Tag = 'ViewStatusAddition'; Type = 'ViewStatusRepresentation'; Suffix = 'СостояниеПросмотра' }
'searchControl' = @{ Tag = 'SearchControlAddition'; Type = 'SearchControl'; Suffix = 'УправлениеПоиском' }
}
# Синонимы типа дополнения (для override-карты additions и резолва тип-ключа).
$script:additionKeySynonyms = @{
'searchString' = @('SearchStringAddition','SearchStringRepresentation','строкаПоиска','отображениеСтрокиПоиска')
'viewStatus' = @('ViewStatusAddition','ViewStatusRepresentation','состояниеПросмотра')
'searchControl' = @('SearchControlAddition','SearchControl','управлениеПоиском')
}
# HorizontalLocation: auto (дефолт, тег опускаем) / left / right; forgiving + рус.синонимы.
function Get-HLocation {
param($el)
$v = if ($el -and $el.PSObject.Properties['horizontalLocation']) { $el.horizontalLocation } else { $null }
if (-not $v) { return $null }
switch -Regex ("$v".ToLower()) {
'^(auto|авто)$' { return $null } # дефолт — не эмитим
'^(left|слева|лево)$' { return 'Left' }
'^(right|справа|право)$' { return 'Right' }
'^(center|центр|по центру)$' { return 'Center' }
default { return "$v" }
}
}
# Тело дополнения: AdditionSource + свойства (как у поля) + companions. $props может быть $null
# (стандартное дополнение без отклонений). Порядок 1С-толерантен (diff порядок-независим).
function Emit-AdditionBody {
param($props, [string]$source, [string]$srcType, [string]$addName, [string]$indent)
$inner = "$indent`t"
X "$inner<AdditionSource>"
X "$inner`t<Item>$source</Item>"
X "$inner`t<Type>$srcType</Type>"
X "$inner</AdditionSource>"
if ($props) {
if ($props.PSObject.Properties['title'] -and $props.title) { Emit-MLText -tag "Title" -text $props.title -indent $inner }
Emit-CommonFlags -el $props -indent $inner
if ($props.tooltip) { Emit-MLText -tag "ToolTip" -text $props.tooltip -indent $inner }
if ($props.tooltipRepresentation) { X "$inner<ToolTipRepresentation>$($props.tooltipRepresentation)</ToolTipRepresentation>" }
$hl = Get-HLocation $props; if ($hl) { X "$inner<HorizontalLocation>$hl</HorizontalLocation>" }
Emit-Layout -el $props -indent $inner
Emit-Appearance -el $props -indent $inner -profile 'field'
}
Emit-Companion -tag "ContextMenu" -name "${addName}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${addName}РасширеннаяПодсказка" -indent $inner
}
# Кастомное дополнение (тип-элемент в commandBar): source дефолтит в текущую таблицу.
function Emit-Addition {
param($el, [string]$name, [int]$id, [string]$typeKey, [string]$indent)
$map = $script:additionTypeMap[$typeKey]
$source = if ($el.source) { "$($el.source)" } elseif ($script:currentTableName) { $script:currentTableName } else { '' }
X "$indent<$($map.Tag) name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
Emit-AdditionBody -props $el -source $source -srcType $map.Type -addName $name -indent $indent
X "$indent</$($map.Tag)>"
}
# Стандартное табличное дополнение (авто-генерация на уровне таблицы). $override — объект отклонений
# из per-table карты additions (или $null = чистый дефолт).
function Emit-TableAddition {
param([string]$typeKey, [string]$tableName, [string]$indent, $override = $null)
$map = $script:additionTypeMap[$typeKey]
$addName = "$tableName$($map.Suffix)"
$id = New-Id
X "$indent<$($map.Tag) name=`"$addName`" id=`"$id`">"
Emit-AdditionBody -props $override -source $tableName -srcType $map.Type -addName $addName -indent $indent
X "$indent</$($map.Tag)>"
}
# Прочитать override-объект для типа дополнения из per-table карты additions (с синонимами).
function Get-AdditionOverride {
param($additions, [string]$typeKey)
if ($null -eq $additions) { return $null }
foreach ($k in @($typeKey) + $script:additionKeySynonyms[$typeKey]) {
$p = $additions.PSObject.Properties[$k]
if ($p) { return $p.Value }
}
return $null
}
function Emit-Element {
param($el, [string]$indent, [bool]$inCmdBar = $false)
# Companion-панели (объект/массив-значение) → commandBar/contextMenu, до тип-синонимов.
Normalize-PanelSynonyms $el
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
# Maps any synonym to canonical short DSL key.
# commandBar/autoCommandBar/КоманднаяПанель → тип-элемент ТОЛЬКО при строковом значении (имя);
# объект/массив уже отнесён к панель-свойству выше.
$strOnlyKeys = @('commandBar','autoCommandBar','КоманднаяПанель')
$synonyms = @{
"commandBar" = "cmdBar"
"autoCommandBar" = "autoCmdBar"
"КоманднаяПанель" = "cmdBar"
"InputField" = "input"
"ПолеВвода" = "input"
"CheckBoxField" = "check"
"ПолеФлажка" = "check"
"RadioButtonField" = "radio"
"ПолеПереключателя" = "radio"
"radioButton" = "radio"
"PictureField" = "picField"
"ПолеКартинки" = "picField"
"LabelField" = "labelField"
"ПолеНадписи" = "labelField"
"CalendarField" = "calendar"
"ПолеКалендаря" = "calendar"
"LabelDecoration" = "label"
"Надпись" = "label"
"PictureDecoration" = "picture"
"Картинка" = "picture"
"UsualGroup" = "group"
"Группа" = "group"
"ОбычнаяГруппа" = "group"
"ColumnGroup" = "columnGroup"
"ГруппаКолонок" = "columnGroup"
"Pages" = "pages"
"ГруппаСтраниц" = "pages"
"Page" = "page"
"Страница" = "page"
"Table" = "table"
"Таблица" = "table"
"Button" = "button"
"Кнопка" = "button"
"Popup" = "popup"
"ВсплывающееМеню" = "popup"
# Дополнения командной панели таблицы (тип-как-ключ) — forgiving: XML-тег/Type/рус.имя → канон
"SearchStringAddition" = "searchString"
"SearchStringRepresentation" = "searchString"
"строкаПоиска" = "searchString"
"отображениеСтрокиПоиска" = "searchString"
"Отображение строки поиска" = "searchString"
"ViewStatusAddition" = "viewStatus"
"ViewStatusRepresentation" = "viewStatus"
"состояниеПросмотра" = "viewStatus"
"Состояние просмотра" = "viewStatus"
"SearchControlAddition" = "searchControl"
"SearchControl" = "searchControl"
"управлениеПоиском" = "searchControl"
"Управление поиском" = "searchControl"
# Спец-поля (документ/датчик) — XML-имя/рус. → канон
"SpreadSheetDocumentField" = "spreadsheet"
"ПолеТабличногоДокумента" = "spreadsheet"
"HTMLDocumentField" = "html"
"ПолеHTMLДокумента" = "html"
"TextDocumentField" = "textDoc"
"ПолеТекстовогоДокумента" = "textDoc"
"FormattedDocumentField" = "formattedDoc"
"ПолеФорматированногоДокумента" = "formattedDoc"
"ProgressBarField" = "progressBar"
"ПолеИндикатора" = "progressBar"
"TrackBarField" = "trackBar"
"ПолеПолосыРегулирования" = "trackBar"
"ChartField" = "chart"
"ПолеДиаграммы" = "chart"
"GanttChartField" = "ganttChart"
"ПолеДиаграммыГанта" = "ganttChart"
"GraphicalSchemaField" = "graphicalSchema"
"ПолеГрафическойСхемы" = "graphicalSchema"
"PlannerField" = "planner"
"ПолеПланировщика" = "planner"
"PeriodField" = "periodField"
"ПолеПериода" = "periodField"
"DendrogramField" = "dendrogram"
"ПолеДендрограммы" = "dendrogram"
}
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
if ($strOnlyKeys -contains $pair.Key -and -not ($el.($pair.Key) -is [string])) { continue }
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
# Синонимы ключей-свойств (русские имена 1С → канон. англ.). Case/space-insensitive.
# Канон побеждает: если задан и русский, и англ. ключ — англ. остаётся, русский отбрасываем.
foreach ($pn in @($el.PSObject.Properties.Name)) {
$norm = ($pn -replace '\s','').ToLower()
$canon = $script:propSynonyms[$norm]
if ($canon -and $pn -ne $canon) {
if ($null -eq $el.PSObject.Properties[$canon]) {
$val = $el.($pn)
$el | Add-Member -NotePropertyName $canon -NotePropertyValue $val -Force
}
$el.PSObject.Properties.Remove($pn) | Out-Null
}
}
# Determine element type from key
$typeKey = $null
$xmlTag = $null
# picture/picField — НИЗКИЙ приоритет: 'picture' это и тип (PictureDecoration), и свойство-иконка
# у popup/button/cmdBar. Тип-ключ владельца (popup/button/…) должен выиграть.
# pages/page ПЕРЕД group: у Page/Pages ключ 'group' — это направление раскладки детей
# (<Group>Horizontal</Group>), а не тип UsualGroup. Реальная UsualGroup ключа page/pages не несёт.
foreach ($key in @("columnGroup","buttonGroup","pages","page","group","input","check","radio","label","labelField","table","button","calendar","cmdBar","popup","searchString","viewStatus","searchControl","picField","picture","spreadsheet","html","textDoc","formattedDoc","progressBar","trackBar","chart","ganttChart","graphicalSchema","planner","periodField","dendrogram")) {
if ($el.$key -ne $null) {
$typeKey = $key
break
}
}
if (-not $typeKey) {
Write-Warning "Unknown element type, skipping"
return
}
# Validate known keys — warn about typos and unknown properties
$knownKeys = @{
# type keys
"group"=1;"columnGroup"=1;"buttonGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1
"button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1
# спец-поля (документ/датчик/диаграмма) — тип-ключи + типоспец. скаляры
"spreadsheet"=1;"html"=1;"textDoc"=1;"formattedDoc"=1;"progressBar"=1;"trackBar"=1
"chart"=1;"ganttChart"=1;"graphicalSchema"=1;"planner"=1;"periodField"=1;"dendrogram"=1;"ganttTable"=1
"showPercent"=1;"largeStep"=1;"markingStep"=1;"step"=1
"horizontalScrollBar"=1;"viewScalingMode"=1;"output"=1;"selectionShowMode"=1;"protection"=1
"edit"=1;"showGrid"=1;"showGroups"=1;"showHeaders"=1;"showRowAndColumnNames"=1;"showCellNames"=1
"pointerType"=1;"drawingSelectionShowMode"=1;"warningOnEditRepresentation"=1;"markingAppearance"=1
# report-form контекст (generic-скаляры элементов)
"horizontalSpacing"=1;"representationInContextMenu"=1;"settingsNamedItemDetailedRepresentation"=1
# хвост: высота элемента списка / ширина выпадающего списка / картинка кнопки выбора / прозрачный пиксель
"itemHeight"=1;"dropListWidth"=1;"choiceButtonPicture"=1;"transparentPixel"=1
# хвост CI-форм: динамический заголовок / расширенное редактирование / высота таблицы
"titleDataPath"=1;"extendedEdit"=1;"maxRowsCount"=1;"autoMaxRowsCount"=1;"heightControlVariant"=1
"warningOnEdit"=1;"nonselectedPictureText"=1;"editTextUpdate"=1;"footerText"=1
# columnGroup-specific
"showInHeader"=1
# radio-specific
"radioButtonType"=1;"choiceList"=1;"columnsCount"=1;"checkBoxType"=1;"editMode"=1
# naming & binding
"name"=1;"path"=1;"title"=1;"tooltip"=1;"tooltipRepresentation"=1;"extendedTooltip"=1
# companion-панели (свойства): командная панель + контекстное меню
"commandBar"=1;"contextMenu"=1
# источник команд группы/панели (ButtonGroup/CommandBar)
"commandSource"=1
# visibility & state
"visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1
# events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости)
"events"=1;"on"=1;"handlers"=1
# layout
"titleLocation"=1;"representation"=1;"width"=1;"height"=1
"horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1
"maxWidth"=1;"maxHeight"=1
"groupHorizontalAlign"=1;"groupVerticalAlign"=1;"horizontalAlign"=1
# input-specific
"multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1
"spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1
"textEdit"=1
"wrap"=1;"openButton"=1;"listChoiceMode"=1;"showInFooter"=1
"extendedEditMultipleValues"=1;"chooseType"=1;"autoCellHeight"=1
"choiceButtonRepresentation"=1;"footerHorizontalAlign"=1;"headerHorizontalAlign"=1
"headerDataPath"=1;"headerFormat"=1;"currentRowUse"=1
"format"=1;"editFormat"=1;"choiceParameters"=1;"choiceParameterLinks"=1;"typeLink"=1
# label/hyperlink
"hyperlink"=1;"formatted"=1
# group-specific
"collapsedTitle"=1;"showTitle"=1;"united"=1;"collapsed"=1;"behavior"=1
# hierarchy
"children"=1;"columns"=1
# table-specific
"changeRowSet"=1;"changeRowOrder"=1;"autoInsertNewRow"=1;"rowFilter"=1;"header"=1;"footer"=1
"commandBarLocation"=1;"searchStringLocation"=1;"viewStatusLocation"=1;"searchControlLocation"=1
"excludedCommands"=1
"choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1
"rowPictureDataPath"=1;"tableAutofill"=1;"heightInTableRows"=1
"multipleChoice"=1;"searchOnInput"=1;"shortcut"=1
"rowSelectionMode"=1;"verticalLines"=1;"horizontalLines"=1
# dynamic-list table block
"defaultItem"=1;"useAlternationRowColor"=1;"fileDragMode"=1;"autoRefresh"=1
"autoRefreshPeriod"=1;"choiceFoldersAndItems"=1;"restoreCurrentRow"=1;"showRoot"=1
"allowRootChoice"=1;"updateOnDataChange"=1;"allowGettingCurrentRowURL"=1
"userSettingsGroup"=1;"rowsPicture"=1
# calendar-specific
"selectionMode"=1;"showCurrentDate"=1;"widthInMonths"=1;"heightInMonths"=1;"showMonthsPanel"=1
# pages-specific
"pagesRepresentation"=1
# button-specific
"type"=1;"command"=1;"commandName"=1;"stdCommand"=1;"parameter"=1;"defaultButton"=1;"locationInCommandBar"=1;"displayImportance"=1
# picture/decoration
"src"=1;"valuesPicture"=1;"loadTransparent"=1;"headerPicture"=1;"footerPicture"=1
# cmdBar-specific
"autofill"=1
# AutoCommandBar-маркер (autofill heuristic) на элементе/таблице
"autoCmdBar"=1
# дополнения командной панели таблицы (тип-ключи + свойства)
"searchString"=1;"viewStatus"=1;"searchControl"=1;"source"=1;"horizontalLocation"=1;"additions"=1
# generic-скаляры (pass-through) + точечные
"verticalAlign"=1;"throughAlign"=1;"enableContentChange"=1;"pictureSize"=1;"titleHeight"=1
"childItemsWidth"=1;"showLeftMargin"=1;"cellHyperlink"=1;"viewMode"=1;"verticalScrollBar"=1
"rowInputMode"=1;"mask"=1;"createButton"=1;"fixingInTable"=1;"verticalSpacing"=1
# InputField choice-скаляры
"choiceListButton"=1;"quickChoice"=1;"autoChoiceIncomplete"=1
"choiceForm"=1;"choiceHistoryOnInput"=1;"footerDataPath"=1;"minValue"=1;"maxValue"=1
# Button — пометка toggle-кнопки (ключ 'checked', не 'check' — во избежание конфликта с типом)
"checked"=1
}
# Оформление (цвета/шрифты/граница) — авто-регистрация из самих структур, чтобы allowlist
# не дрейфовал при добавлении новых ключей/синонимов. Канонические + forgiving-синонимы.
foreach ($k in $script:appearanceSpec.Keys) { $knownKeys[$k] = 1 }
foreach ($k in $script:appearanceSynonyms.Keys) { $knownKeys[$k] = 1 }
foreach ($k in $script:propSynonyms.Keys) { $knownKeys[$k] = 1 }
foreach ($p in $el.PSObject.Properties) {
if ($p.Name -like '_*') { continue } # внутренние маркеры (напр. _dynList)
if (-not $knownKeys.ContainsKey($p.Name)) {
Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys."
}
}
$name = Get-ElementName -el $el -typeKey $typeKey
Assert-UniqueName -name $name -seen $script:seenElementNames -kind 'element'
$id = New-Id
switch ($typeKey) {
"group" { Emit-Group -el $el -name $name -id $id -indent $indent }
"columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent }
"buttonGroup" { Emit-ButtonGroup -el $el -name $name -id $id -indent $indent }
"input" { Emit-Input -el $el -name $name -id $id -indent $indent }
"check" { Emit-Check -el $el -name $name -id $id -indent $indent }
"radio" { Emit-Radio -el $el -name $name -id $id -indent $indent }
"label" { Emit-Label -el $el -name $name -id $id -indent $indent }
"labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent }
"table" { Emit-Table -el $el -name $name -id $id -indent $indent }
"pages" { Emit-Pages -el $el -name $name -id $id -indent $indent }
"page" { Emit-Page -el $el -name $name -id $id -indent $indent }
"button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar }
"picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent }
"searchString" { Emit-Addition -el $el -name $name -typeKey "searchString" -id $id -indent $indent }
"viewStatus" { Emit-Addition -el $el -name $name -typeKey "viewStatus" -id $id -indent $indent }
"searchControl" { Emit-Addition -el $el -name $name -typeKey "searchControl" -id $id -indent $indent }
"picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent }
"calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent }
"cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent }
"popup" { Emit-Popup -el $el -name $name -id $id -indent $indent }
"spreadsheet" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "SpreadSheetDocumentField" -typeKey "spreadsheet" }
"html" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "HTMLDocumentField" -typeKey "html" }
"textDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TextDocumentField" -typeKey "textDoc" }
"formattedDoc" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "FormattedDocumentField" -typeKey "formattedDoc" }
"progressBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ProgressBarField" -typeKey "progressBar" }
"trackBar" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "TrackBarField" -typeKey "trackBar" }
"chart" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "ChartField" -typeKey "chart" }
"graphicalSchema" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "GraphicalSchemaField" -typeKey "graphicalSchema" }
"planner" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PlannerField" -typeKey "planner" }
"periodField" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "PeriodField" -typeKey "periodField" }
"dendrogram" { Emit-SimpleField -el $el -name $name -id $id -indent $indent -xmlTag "DendrogramField" -typeKey "dendrogram" }
"ganttChart" { Emit-GanttChart -el $el -name $name -id $id -indent $indent }
}
}
# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X").
# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки).
# Значение DSL: скаляр bool → только <xr:Common>; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения.
# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом.
function Emit-XrFlag {
param([string]$tag, $val, [string]$indent)
if ($null -eq $val) { return }
if ($val -is [bool]) {
X "$indent<$tag>"
X "$indent`t<xr:Common>$(if ($val){'true'}else{'false'})</xr:Common>"
X "$indent</$tag>"
return
}
# объектная форма { common, roles }
$common = if ($null -ne $val.common) { [bool]$val.common } else { $false }
X "$indent<$tag>"
X "$indent`t<xr:Common>$(if ($common){'true'}else{'false'})</xr:Common>"
if ($val.roles) {
foreach ($r in $val.roles.PSObject.Properties) {
# Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role.".
# Роль по GUID (заимствованная/расширение — name="<guid>" без префикса) эмитим как есть.
$rname = "$($r.Name)" -replace '^(Role|Роль)\.', ''
if ($rname -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $rname = "Role.$rname" }
$rval = if ([bool]$r.Value) { 'true' } else { 'false' }
X "$indent`t<xr:Value name=`"$rname`">$rval</xr:Value>"
}
}
X "$indent</$tag>"
}
function Emit-CommonFlags {
param($el, [string]$indent)
if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" }
if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent }
if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" }
if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" }
}
# Общие layout-свойства — применимы ко всем элементам. Порядок согласован с
# историческим выводом input/label, чтобы не сдвигать существующие снапшоты.
# -skipHeight: подавить <Height> (зарезервирован; Table теперь эмитит <Height> generic-ом + свой <HeightInTableRows>).
# -multiLineDefault: input без явного autoMaxWidth при multiLine → AutoMaxWidth=false.
# Общие свойства элемента (любой тип, включая Button/cmdBar): default/skip/drag.
function Emit-CommonElementProps {
param($el, [string]$indent)
if ($el.defaultItem -eq $true) { X "$indent<DefaultItem>true</DefaultItem>" }
if ($el.PSObject.Properties['skipOnInput'] -and $null -ne $el.skipOnInput) {
$siv = if ($el.skipOnInput -eq $true) { 'true' } else { 'false' }
X "$indent<SkipOnInput>$siv</SkipOnInput>"
}
# EnableStartDrag — фактическое значение (платформа эмитит и явный false, напр. SpreadSheet)
if ($null -ne $el.enableStartDrag) { X "$indent<EnableStartDrag>$(if ($el.enableStartDrag){'true'}else{'false'})</EnableStartDrag>" }
if ($el.fileDragMode) { X "$indent<FileDragMode>$($el.fileDragMode)</FileDragMode>" }
# Cell-свойства поля в таблице (общие для Input/Label/Picture/CheckBox): захват «как есть»
foreach ($p in @(@('showInHeader','ShowInHeader'), @('showInFooter','ShowInFooter'), @('autoCellHeight','AutoCellHeight'))) {
if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
}
# Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD)
if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-Xml "$($el.headerDataPath)")</HeaderDataPath>" }
if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" }
if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" }
# Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD)
if ($el.headerFormat) { Emit-MLText -tag "HeaderFormat" -text $el.headerFormat -indent $indent }
}
# Картинка-ссылка с прозрачностью (HeaderPicture/FooterPicture/ValuesPicture/Page Picture).
# Платформа ВСЕГДА эмитит <xr:LoadTransparent> → пишем всегда (false по умолчанию).
# Значение: скаляр (Ref) ИЛИ объект {src, loadTransparent, transparentPixel}.
# src с префиксом "abs:" → встроенная картинка <xr:Abs>; иначе именованная/стилевая <xr:Ref>.
function Emit-PictureRef {
param($val, [string]$picTag, [string]$indent)
if (-not $val) { return }
$src = $null; $lt = $false; $tpx = $null
if ($val -is [string]) { $src = $val }
else { $src = $val.src; if ($val.loadTransparent -eq $true) { $lt = $true }; $tpx = $val.transparentPixel }
if (-not $src) { return }
$srcStr = "$src"
X "$indent<$picTag>"
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
X "$indent</$picTag>"
}
# Картинки заголовка/подвала колонки поля — по схеме сразу после <EditMode>,
# перед тип-специфичными элементами и layout (порядок XDTO строгий именно здесь).
function Emit-ColumnPics {
param($el, [string]$indent)
Emit-PictureRef -val $el.headerPicture -picTag 'HeaderPicture' -indent $indent
Emit-PictureRef -val $el.footerPicture -picTag 'FooterPicture' -indent $indent
}
# <Picture> кнопки/попапа/команды. Дефолт LoadTransparent=true, отклонение false
# (обратная конвенция относительно header/values-картинок). Прощающий ввод:
# принимает скаляр (Ref) ИЛИ объект {src, loadTransparent} — на случай если модель
# опишет картинку объектно по аналогии с headerPicture. $elemLt — legacy
# элемент-уровневый ключ loadTransparent (используется, если в объекте флаг не задан).
function Emit-CommandPicture {
param($pic, $elemLt, [string]$indent)
if (-not $pic) { return }
$src = $null; $lt = $null; $tpx = $null
if ($pic -is [string]) { $src = $pic }
else { $src = $pic.src; if ($null -ne $pic.loadTransparent) { $lt = [bool]$pic.loadTransparent }; $tpx = $pic.transparentPixel }
if (-not $src) { return }
if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt }
$srcStr = "$src"
X "$indent<Picture>"
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>"
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
X "$indent</Picture>"
}
# --- Оформление элемента: цвета / шрифты / граница ---
# Прямые свойства элемента (<TextColor>/<Font>/<Border> + header/footer-варианты у полей).
# Ключи DSL — англ. camelCase 1:1 с тегами; принимаем рус. синонимы (forgiving).
# Значения: цвет — verbatim-строка (style:/web:/sys:/win:/#RRGGBB); шрифт — строка-ref или
# объект-атрибуты; граница — строка-ref или объект {width,style|ref}. Порядок тегов — XSD (профиль).
$script:appearanceSpec = @{
titleTextColor = @{ tag='TitleTextColor'; kind='color' }
titleBackColor = @{ tag='TitleBackColor'; kind='color' }
titleFont = @{ tag='TitleFont'; kind='font' }
footerTextColor = @{ tag='FooterTextColor'; kind='color' }
footerBackColor = @{ tag='FooterBackColor'; kind='color' }
footerFont = @{ tag='FooterFont'; kind='font' }
textColor = @{ tag='TextColor'; kind='color' }
backColor = @{ tag='BackColor'; kind='color' }
borderColor = @{ tag='BorderColor'; kind='color' }
border = @{ tag='Border'; kind='border'}
font = @{ tag='Font'; kind='font' }
}
# Рус. синоним (lower) → canonical key
$script:appearanceSynonyms = @{
'цветтекста'='textColor'; 'цветфона'='backColor'; 'цветрамки'='borderColor'
'цветтекстазаголовка'='titleTextColor'; 'цветфоназаголовка'='titleBackColor'; 'шрифтзаголовка'='titleFont'
'цветтекстаподвала'='footerTextColor'; 'цветфонаподвала'='footerBackColor'; 'шрифтподвала'='footerFont'
'шрифт'='font'; 'рамка'='border'
}
# Синонимы ключей-свойств: русские имена свойств 1С (как в Конфигураторе) → канон. англ. ключ.
# Ключи карты нормализованы (lowercase, без пробелов); сопоставление в Normalize-PropSynonyms тоже.
# Прощающий ввод: модель может писать свойство по-русски. Англ. ключ работает всегда (это доп. слой).
# Видимость/Доступность НЕ включаем — наш hidden/disabled инвертирован, был бы баг семантики.
$script:propSynonyms = @{
'пометка'='checked'
'кнопкавыбора'='choiceButton'; 'кнопкаочистки'='clearButton'; 'кнопкарегулирования'='spinButton'
'кнопкавыпадающегосписка'='dropListButton'; 'кнопкасписковоговыбора'='choiceListButton'
'кнопкаоткрытия'='openButton'; 'кнопкапоумолчанию'='defaultButton'
'быстрыйвыбор'='quickChoice'; 'формавыбора'='choiceForm'; 'историявыборапривводе'='choiceHistoryOnInput'
'выборгруппиэлементов'='choiceFoldersAndItems'; 'фиксациявтаблице'='fixingInTable'
'путькданнымподвала'='footerDataPath'; 'автоотметканезаполненного'='markIncomplete'
'многострочныйрежим'='multiLine'; 'режимпароля'='passwordMode'; 'переноспословам'='wrap'
'расположениезаголовка'='titleLocation'; 'пропускатьпривводе'='skipOnInput'
'заголовок'='title'; 'ширина'='width'; 'высота'='height'; 'подсказкаввода'='inputHint'
}
# Профили порядка тегов по базовым типам (XSD-последовательность)
$script:appOrderField = @('titleTextColor','titleBackColor','titleFont','footerTextColor','footerBackColor','footerFont','textColor','backColor','borderColor','border','font')
$script:appOrderDecoration = @('textColor','font','backColor','borderColor','border')
$script:appOrderButton = @('textColor','backColor','borderColor','font')
# Простые скаляры элемента (pass-through: captured/emitted «как есть»). Только НЕ-перекрывающиеся
# теги (не обрабатываемые специфично где-то ещё). kind: bool → true/false; value → строка verbatim.
$script:genericScalars = @(
@{ Tag='VerticalAlign'; Key='verticalAlign'; Kind='value' }
@{ Tag='ThroughAlign'; Key='throughAlign'; Kind='value' }
@{ Tag='EnableContentChange'; Key='enableContentChange'; Kind='bool' }
@{ Tag='PictureSize'; Key='pictureSize'; Kind='value' }
@{ Tag='TitleHeight'; Key='titleHeight'; Kind='value' }
@{ Tag='ChildItemsWidth'; Key='childItemsWidth'; Kind='value' }
@{ Tag='ShowLeftMargin'; Key='showLeftMargin'; Kind='bool' }
@{ Tag='CellHyperlink'; Key='cellHyperlink'; Kind='bool' }
@{ Tag='ViewMode'; Key='viewMode'; Kind='value' }
@{ Tag='VerticalScrollBar'; Key='verticalScrollBar'; Kind='value' }
@{ Tag='RowInputMode'; Key='rowInputMode'; Kind='value' }
@{ Tag='Mask'; Key='mask'; Kind='value' }
@{ Tag='CreateButton'; Key='createButton'; Kind='bool' }
@{ Tag='FixingInTable'; Key='fixingInTable'; Kind='value' }
@{ Tag='VerticalSpacing'; Key='verticalSpacing'; Kind='value' }
# Спец-поля (документ/датчик) — типоспец. enum/bool скаляры pass-through
@{ Tag='HorizontalScrollBar'; Key='horizontalScrollBar'; Kind='value' }
@{ Tag='ViewScalingMode'; Key='viewScalingMode'; Kind='value' }
@{ Tag='Output'; Key='output'; Kind='value' }
@{ Tag='SelectionShowMode'; Key='selectionShowMode'; Kind='value' }
@{ Tag='PointerType'; Key='pointerType'; Kind='value' }
@{ Tag='DrawingSelectionShowMode'; Key='drawingSelectionShowMode'; Kind='value' }
@{ Tag='WarningOnEditRepresentation'; Key='warningOnEditRepresentation'; Kind='value' }
@{ Tag='MarkingAppearance'; Key='markingAppearance'; Kind='value' }
@{ Tag='Protection'; Key='protection'; Kind='bool' }
@{ Tag='Edit'; Key='edit'; Kind='bool' }
@{ Tag='ShowGrid'; Key='showGrid'; Kind='bool' }
@{ Tag='ShowGroups'; Key='showGroups'; Kind='bool' }
@{ Tag='ShowHeaders'; Key='showHeaders'; Kind='bool' }
@{ Tag='ShowRowAndColumnNames'; Key='showRowAndColumnNames'; Kind='bool' }
@{ Tag='ShowCellNames'; Key='showCellNames'; Kind='bool' }
@{ Tag='ShowPercent'; Key='showPercent'; Kind='bool' }
# Report-form контекст: интервал группы / представление кнопки в контекстном меню / детальное представление настройки таблицы
@{ Tag='HorizontalSpacing'; Key='horizontalSpacing'; Kind='value' }
@{ Tag='RepresentationInContextMenu'; Key='representationInContextMenu'; Kind='value' }
@{ Tag='SettingsNamedItemDetailedRepresentation'; Key='settingsNamedItemDetailedRepresentation'; Kind='bool' }
# Хвост: высота элемента списка (radio) / ширина выпадающего списка (input)
@{ Tag='ItemHeight'; Key='itemHeight'; Kind='value' }
@{ Tag='DropListWidth'; Key='dropListWidth'; Kind='value' }
# Хвост CI-форм: динамический заголовок (Page/Group) / расширенное ред. (input) / высота таблицы по строкам
@{ Tag='TitleDataPath'; Key='titleDataPath'; Kind='value' }
@{ Tag='ExtendedEdit'; Key='extendedEdit'; Kind='bool' }
@{ Tag='MaxRowsCount'; Key='maxRowsCount'; Kind='value' }
@{ Tag='AutoMaxRowsCount'; Key='autoMaxRowsCount'; Kind='bool' }
@{ Tag='HeightControlVariant'; Key='heightControlVariant'; Kind='value' }
@{ Tag='EditTextUpdate'; Key='editTextUpdate'; Kind='value' }
# Корпусный хвост: представление управления свёрткой группы / форма кнопки-попапа /
# авто-добавление незаполненной строки / выделение отрицательных / нач. позиция списка /
# высота списка выбора / три состояния флажка / прокрутка страницы при сжатии
@{ Tag='ControlRepresentation'; Key='controlRepresentation'; Kind='value' }
@{ Tag='ShapeRepresentation'; Key='shapeRepresentation'; Kind='value' }
@{ Tag='AutoAddIncomplete'; Key='autoAddIncomplete'; Kind='bool' }
@{ Tag='MarkNegatives'; Key='markNegatives'; Kind='bool' }
@{ Tag='InitialListView'; Key='initialListView'; Kind='value' }
@{ Tag='ChoiceListHeight'; Key='choiceListHeight'; Kind='value' }
@{ Tag='ThreeState'; Key='threeState'; Kind='bool' }
@{ Tag='ScrollOnCompress'; Key='scrollOnCompress'; Kind='bool' }
# Сочетание клавиш — общее свойство (input/group/radio/page/picField/label/table/check; команда — отд. путь, §7)
@{ Tag='Shortcut'; Key='shortcut'; Kind='value' }
# Батч простых скаляров (input/radio/group/picDecoration/button): режим выбора незаполненного,
# равная ширина колонок, выравнивание детей, масштаб/зум картинки, форма/положение картинки кнопки.
# (Table HeaderHeight/FooterHeight/CurrentRowUse — НЕ здесь, а в Emit-Table: pass-through,
# 1С толерантна к порядку детей Table — в корпусе те же теги встречаются в разных позициях.)
@{ Tag='IncompleteChoiceMode'; Key='incompleteChoiceMode'; Kind='value' }
@{ Tag='EqualColumnsWidth'; Key='equalColumnsWidth'; Kind='bool' }
@{ Tag='ChildrenAlign'; Key='childrenAlign'; Kind='value' }
@{ Tag='ImageScale'; Key='imageScale'; Kind='value' }
@{ Tag='Zoomable'; Key='zoomable'; Kind='bool' }
@{ Tag='Shape'; Key='shape'; Kind='value' }
@{ Tag='PictureLocation'; Key='pictureLocation'; Kind='value' }
# Равная ширина элементов (check/radio) / высота заголовка пункта (radio)
@{ Tag='EqualItemsWidth'; Key='equalItemsWidth'; Kind='bool' }
@{ Tag='ItemTitleHeight'; Key='itemTitleHeight'; Kind='value' }
# Спец-режим ввода текста (input, моб.: Email/PhoneNumber/...) — листовой enum-скаляр
@{ Tag='SpecialTextInputMode'; Key='specialTextInputMode'; Kind='value' }
# Ширина пункта (radio/check) / выбор нескольких значений из выпадающего (input)
@{ Tag='ItemWidth'; Key='itemWidth'; Kind='value' }
@{ Tag='ShowCheckBoxesInDropList'; Key='showCheckBoxesInDropList'; Kind='bool' }
@{ Tag='MultipleValueDataPath'; Key='multipleValueDataPath'; Kind='value' }
@{ Tag='MultipleValuePresentDataPath'; Key='multipleValuePresentDataPath'; Kind='value' }
# Режим авто-показа кнопок открытия/очистки (input, enum Auto/Always/FilledOnly/…)
@{ Tag='AutoShowOpenButtonMode'; Key='autoShowOpenButtonMode'; Kind='value' }
@{ Tag='AutoShowClearButtonMode'; Key='autoShowClearButtonMode'; Kind='value' }
# Оформление/картинка множественного выбора (input, редко; цвета — текст-контент, не атрибуты)
@{ Tag='MultipleValuesTextColor'; Key='multipleValuesTextColor'; Kind='value' }
@{ Tag='MultipleValuesBackColor'; Key='multipleValuesBackColor'; Kind='value' }
@{ Tag='MultipleValuePictureShape'; Key='multipleValuePictureShape'; Kind='value' }
@{ Tag='MultipleValuePictureDataPath'; Key='multipleValuePictureDataPath'; Kind='value' }
# Хвост листовых скаляров (по 1 в корпусе): автокоррекция ввода (input) / уникальность команды
# (button) / допуск пустого множ. значения (input) / поведение при гориз. сжатии (table)
@{ Tag='AutoCorrectionOnTextInput'; Key='autoCorrectionOnTextInput'; Kind='value' }
@{ Tag='SpellCheckingOnTextInput'; Key='spellCheckingOnTextInput'; Kind='value' }
@{ Tag='CommandUniqueness'; Key='commandUniqueness'; Kind='bool' }
@{ Tag='AllowInputEmptyMultipleValues';Key='allowInputEmptyMultipleValues';Kind='bool' }
@{ Tag='BehaviorOnHorizontalCompression'; Key='behaviorOnHorizontalCompression'; Kind='value' }
)
function Emit-GenericScalars {
param($el, [string]$indent)
if ($null -eq $el) { return }
foreach ($s in $script:genericScalars) {
$p = $el.PSObject.Properties[$s.Key]
if (-not $p -or $null -eq $p.Value) { continue }
if ($s.Kind -eq 'bool') {
X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>"
} else {
$v = "$($p.Value)"; if ($v -eq '') { continue }
X "$indent<$($s.Tag)>$(Esc-Xml $v)</$($s.Tag)>"
}
}
}
function Get-AppearanceValue {
param($el, [string]$canonical)
if ($null -eq $el) { return $null }
$p = $el.PSObject.Properties[$canonical]
if ($p) { return $p.Value }
foreach ($syn in $script:appearanceSynonyms.Keys) {
if ($script:appearanceSynonyms[$syn] -eq $canonical) {
$pp = $el.PSObject.Properties[$syn]
if ($pp) { return $pp.Value }
}
}
return $null
}
# <Font|TitleFont|FooterFont …> — строка = ref на стиль (kind=StyleItem); объект = атрибуты.
function Emit-FontTag {
param([string]$tag, $val, [string]$indent)
if ($val -is [string]) {
X "$indent<$tag ref=`"$(Esc-Xml $val)`" kind=`"StyleItem`"/>"
return
}
$attrs = @()
foreach ($a in @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')) {
$pp = $val.PSObject.Properties[$a]
if ($pp -and $null -ne $pp.Value) {
$v = $pp.Value
if ($v -is [bool]) { $v = if ($v) {'true'} else {'false'} }
$attrs += "$a=`"$(Esc-Xml "$v")`""
}
}
X "$indent<$tag $($attrs -join ' ')/>"
}
# <Border> — строка/{ref} = из стиля (<Border ref="style:X"/>); {width,style} = явная.
function Emit-BorderTag {
param($val, [string]$indent)
if ($val -is [string]) { X "$indent<Border ref=`"$(Esc-Xml $val)`"/>"; return }
$refP = $val.PSObject.Properties['ref']
if ($refP -and $refP.Value) { X "$indent<Border ref=`"$(Esc-Xml "$($refP.Value)")`"/>"; return }
$width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 }
$style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null }
X "$indent<Border width=`"$width`">"
if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml $style)</v8ui:style>" }
X "$indent</Border>"
}
# ─────────────────────────────────────────────────────────────────────────────
# Planner design-time <Settings xsi:type="pl:Planner"> — встроенный конфиг планировщика
# на реквизите planner-типа. Структурный DSL: items[] + appearance/поведение-скаляры +
# timeScale (уровни шкалы времени) + period. Каждое присутствующее поле → каноничный
# порядок; пропущенное → дефолт (планировщик всегда несёт полный блок). Декомпилятор
# делает полный захват → раундтрип бит-в-бит; ручной авторинг может быть кратким.
$script:PLANNER_NS = 'http://v8.1c.ru/8.3/data/planner'
$script:CHART_NS = 'http://v8.1c.ru/8.2/data/chart'
function PL-Get {
param($o, [string]$k, $def = $null)
if ($null -ne $o -and $o.PSObject.Properties[$k] -and $null -ne $o.$k) { return $o.$k }
return $def
}
function PL-Bool {
param($v)
if ($v -is [bool]) { if ($v) { 'true' } else { 'false' } }
elseif ("$v" -eq 'True') { 'true' }
elseif ("$v" -eq 'False') { 'false' }
else { "$v" }
}
function Emit-PlannerColor {
param([string]$tag, $o, [string]$key, [string]$ind)
X "$ind<pl:$tag>$(Esc-Xml "$(PL-Get $o $key 'auto')")</pl:$tag>"
}
# <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы).
function Emit-PlannerText {
param([string]$tag, $v, [string]$ind)
if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" }
else { X "$ind<pl:$tag>$(Esc-Xml "$v")</pl:$tag>" }
}
# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef";
# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы.
function Test-PlannerRef {
param([string]$v)
return ($v -match '^(Enum|Catalog|Document|ChartOfAccounts|ChartOfCalculationTypes|ChartOfCharacteristicTypes|ExchangePlan|BusinessProcess|Task)\.' -or `
$v -match '\.EnumValue\.' -or $v -match 'EmptyRef$' -or `
$v -match '^(Перечисление|Справочник|Документ|ПланСчетов|ПланВидовХарактеристик|ПланВидовРасчета|ПланОбмена|БизнесПроцесс|Задача)\.')
}
# <pl:value> — nil (нет значения) / xr:DesignTimeRef (ссылка) / xs:string (строка/прочее).
function Emit-PlannerValue {
param($v, [string]$ind)
if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return }
$t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' }
X "$ind<pl:value xsi:type=`"$t`">$(Esc-Xml "$v")</pl:value>"
}
function Emit-PlannerFont {
param($o, [string]$ind)
$f = PL-Get $o 'font' $null
if ($null -eq $f) { X "$ind<pl:font kind=`"AutoFont`"/>"; return }
Emit-FontTag -tag 'pl:font' -val $f -indent $ind
}
function Emit-PlannerBorder {
param($o, [string]$ind, [string]$key = 'border')
$b = PL-Get $o $key $null
$bw = if ($b) { PL-Get $b 'width' 1 } else { 1 }
$bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' }
X "$ind<pl:border width=`"$bw`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$bs")</v8ui:style>"
X "$ind</pl:border>"
}
function Emit-PlannerLevel {
param($lv, [string]$cns, [string]$ind)
$li = "$ind`t"
X "$ind<level xmlns=`"$cns`">"
X "$li<measure>$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")</measure>"
X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>"
X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>"
$line = PL-Get $lv 'line' $null
$lw = if ($line) { PL-Get $line 'width' 1 } else { 1 }
$lg = if ($line) { PL-Get $line 'gap' $false } else { $false }
$lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' }
X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">"
X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$lst")</v8ui:style>"
X "$li</line>"
X "$li<scaleColor>$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>"
X "$li<dayFormatRule>$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>"
$fmt = PL-Get $lv 'format' $null
if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } }
X "$li<format>"
Emit-MLItems -val $fmt -indent "$li`t"
X "$li</format>"
$labels = PL-Get $lv 'labels' $null
$ticks = if ($labels) { PL-Get $labels 'ticks' 0 } else { 0 }
X "$li<labels>"
X "$li`t<ticks>$ticks</ticks>"
X "$li</labels>"
X "$li<backColor>$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")</backColor>"
X "$li<textColor>$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")</textColor>"
X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>"
X "$ind</level>"
}
function Emit-PlannerTimeScale {
param($ts, [string]$ind)
$cns = $script:CHART_NS
$ci = "$ind`t"
X "$ind<pl:timeScale>"
X "$ci<placement xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>"
$levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() }
if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт
foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci }
$transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false }
X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>"
X "$ci<backColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>"
X "$ci<textColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>"
X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>"
X "$ind</pl:timeScale>"
}
function Emit-PlannerItem {
param($it, [string]$ind)
X "$ind<pl:item>"
$ii = "$ind`t"
Emit-PlannerValue (PL-Get $it 'value' $null) $ii
Emit-PlannerText 'text' (PL-Get $it 'text' '') $ii
Emit-PlannerText 'tooltip' (PL-Get $it 'tooltip' '') $ii
X "$ii<pl:begin>$(PL-Get $it 'begin' '0001-01-01T00:00:00')</pl:begin>"
X "$ii<pl:end>$(PL-Get $it 'end' '0001-01-01T00:00:00')</pl:end>"
Emit-PlannerColor 'borderColor' $it 'borderColor' $ii
Emit-PlannerColor 'backColor' $it 'backColor' $ii
Emit-PlannerColor 'textColor' $it 'textColor' $ii
Emit-PlannerFont $it $ii
X "$ii<pl:dimensionValues/>"
X "$ii<pl:replacementDate>$(PL-Get $it 'replacementDate' '0001-01-01T00:00:00')</pl:replacementDate>"
X "$ii<pl:deleted>$(PL-Bool (PL-Get $it 'deleted' $false))</pl:deleted>"
$id = PL-Get $it 'id' $null
if ($null -eq $id) { $id = [guid]::NewGuid().ToString() }
X "$ii<pl:id>$id</pl:id>"
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>"
Emit-PlannerBorder $it $ii 'border'
X "$ii<pl:editMode>$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>"
X "$ind</pl:item>"
}
# Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные
# элементы (UI: колонка «Элементы» у элемента). Порядок: value, text, цвета, font,
# вложенные элементы, showOnlySubordinatesAreas, textFormatted.
function Emit-PlannerDimElement {
param($el, [string]$ind)
X "$ind<pl:item>"
$ii = "$ind`t"
Emit-PlannerValue (PL-Get $el 'value' $null) $ii
Emit-PlannerText 'text' (PL-Get $el 'text' '') $ii
Emit-PlannerColor 'borderColor' $el 'borderColor' $ii
Emit-PlannerColor 'backColor' $el 'backColor' $ii
Emit-PlannerColor 'textColor' $el 'textColor' $ii
Emit-PlannerFont $el $ii
foreach ($sub in @(PL-Get $el 'elements' @())) { Emit-PlannerDimElement $sub $ii }
X "$ii<pl:showOnlySubordinatesAreas>$(PL-Bool (PL-Get $el 'showOnlySubordinatesAreas' $true))</pl:showOnlySubordinatesAreas>"
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $el 'textFormatted' $false))</pl:textFormatted>"
X "$ind</pl:item>"
}
# Измерение планировщика (<pl:dimension>) — объект разреза + его элементы.
function Emit-PlannerDimension {
param($d, [string]$ind)
X "$ind<pl:dimension>"
$di = "$ind`t"
Emit-PlannerValue (PL-Get $d 'value' $null) $di
Emit-PlannerText 'text' (PL-Get $d 'text' '') $di
Emit-PlannerColor 'borderColor' $d 'borderColor' $di
Emit-PlannerColor 'backColor' $d 'backColor' $di
Emit-PlannerColor 'textColor' $d 'textColor' $di
Emit-PlannerFont $d $di
foreach ($el in @(PL-Get $d 'elements' @())) { Emit-PlannerDimElement $el $di }
X "$di<pl:textFormatted>$(PL-Bool (PL-Get $d 'textFormatted' $false))</pl:textFormatted>"
X "$ind</pl:dimension>"
}
function Emit-PlannerSettings {
param($pl, [string]$ind)
X "$ind<Settings xmlns:pl=`"$($script:PLANNER_NS)`" xsi:type=`"pl:Planner`">"
$si = "$ind`t"
foreach ($it in @(PL-Get $pl 'items' @())) { Emit-PlannerItem $it $si }
foreach ($d in @(PL-Get $pl 'dimensions' @())) { Emit-PlannerDimension $d $si }
Emit-PlannerColor 'borderColor' $pl 'borderColor' $si
Emit-PlannerColor 'backColor' $pl 'backColor' $si
Emit-PlannerColor 'textColor' $pl 'textColor' $si
Emit-PlannerColor 'lineColor' $pl 'lineColor' $si
Emit-PlannerFont $pl $si
X "$si<pl:beginOfRepresentationPeriod>$(PL-Get $pl 'beginOfRepresentationPeriod' '0001-01-01T00:00:00')</pl:beginOfRepresentationPeriod>"
X "$si<pl:endOfRepresentationPeriod>$(PL-Get $pl 'endOfRepresentationPeriod' '0001-01-01T00:00:00')</pl:endOfRepresentationPeriod>"
X "$si<pl:alignElementsOfTimeScale>$(PL-Bool (PL-Get $pl 'alignElementsOfTimeScale' $true))</pl:alignElementsOfTimeScale>"
X "$si<pl:displayTimeScaleWrapHeaders>$(PL-Bool (PL-Get $pl 'displayTimeScaleWrapHeaders' $true))</pl:displayTimeScaleWrapHeaders>"
X "$si<pl:displayWrapHeaders>$(PL-Bool (PL-Get $pl 'displayWrapHeaders' $true))</pl:displayWrapHeaders>"
$wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null
if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } }
Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si
X "$si<pl:periodicVariantUnit>$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>"
X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>"
X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>"
X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>"
Emit-PlannerTimeScale (PL-Get $pl 'timeScale' $null) $si
$period = PL-Get $pl 'period' $null
if ($period) {
X "$si<pl:period>"
X "$si`t<pl:begin>$(PL-Get $period 'begin' '0001-01-01T00:00:00')</pl:begin>"
X "$si`t<pl:end>$(PL-Get $period 'end' '0001-01-01T00:00:00')</pl:end>"
X "$si</pl:period>"
}
X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>"
X "$si<pl:itemsTimeRepresentation>$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>"
X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>"
X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>"
X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>"
X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>"
X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>"
X "$si<pl:fixDimensionsHeader>$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>"
X "$si<pl:fixTimeScaleHeader>$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>"
Emit-PlannerBorder $pl $si 'border'
X "$si<pl:newItemsTextType>$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>"
X "$ind</Settings>"
}
# ─────────────────────────────────────────────────────────────────────────────
# Chart design-time <Settings xsi:type="d4p1:Chart"> — генерик-эмиттер (зеркало
# Build-ChartNode декомпилятора). Тип узла → форма XML: ML-поля (по имени), серии
# (массив, повтор тега), line/border/font (по ключам), attrs-узлы (gaugeQualityBands),
# иначе вложенный объект/скаляр. Порядок ключей = порядок эмиссии (раундтрип).
$script:CHART_ML_FIELDS = @{ 'title'=1;'lbFormat'=1;'lbpFormat'=1;'vsFormat'=1;'dtFormat'=1;'dataSourceDescription'=1;'labelFormat'=1;'text'=1 }
$script:CHART_ATTR_FIELDS = @{ 'gaugeQualityBands'=1 }
$script:CHART_FONT_KEYS = @('ref','faceName','height','bold','italic','underline','strikeout','kind','scale')
function Get-Keys { param($o) if ($o -is [System.Collections.IDictionary]) { return @($o.Keys) } else { return @($o.PSObject.Properties.Name) } }
function Get-Prop { param($o, [string]$k) if ($o -is [System.Collections.IDictionary]) { return $o[$k] } else { $p = $o.PSObject.Properties[$k]; if ($p) { return $p.Value } else { return $null } } }
function Emit-ChartNode {
param([string]$name, $val, [string]$ind)
if ($script:CHART_ML_FIELDS.Contains($name)) {
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
X "$ind<d4p1:$name>"; Emit-MLItems -val $val -indent "$ind`t"; X "$ind</d4p1:$name>"; return
}
if (($val -is [System.Collections.IList]) -and ($val -isnot [string])) {
foreach ($e in $val) { Emit-ChartNode $name $e $ind }
return
}
if (($val -is [System.Management.Automation.PSCustomObject]) -or ($val -is [System.Collections.IDictionary])) {
$keys = Get-Keys $val
if ($script:CHART_ATTR_FIELDS.Contains($name)) {
$attrs = @(); foreach ($k in $keys) { $v = Get-Prop $val $k; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$k=`"$(Esc-Xml "$v")`"" }
X "$ind<d4p1:$name $($attrs -join ' ')/>"; return
}
if ($keys -contains 'gap') {
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$st")</v8ui:style>"
X "$ind</d4p1:$name>"; return
}
if (($keys -contains 'style') -and ($keys -contains 'width')) {
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
X "$ind<d4p1:$name width=`"$w`">"
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$st")</v8ui:style>"
X "$ind</d4p1:$name>"; return
}
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
if ($isFont) {
$attrs = @(); foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $v = Get-Prop $val $fk; if ($v -is [bool]) { $v = PL-Bool $v }; $attrs += "$fk=`"$(Esc-Xml "$v")`"" } }
X "$ind<d4p1:$name $($attrs -join ' ')/>"; return
}
if (@($keys).Count -eq 0) { X "$ind<d4p1:$name/>"; return }
X "$ind<d4p1:$name>"
foreach ($k in $keys) { Emit-ChartNode $k (Get-Prop $val $k) "$ind`t" }
X "$ind</d4p1:$name>"
return
}
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
X "$ind<d4p1:$name>$(Esc-Xml "$val")</d4p1:$name>"
}
function Emit-ChartSettings {
param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart')
X "$ind<Settings xmlns:d4p1=`"$($script:CHART_NS)`" xsi:type=`"$ctype`">"
foreach ($k in (Get-Keys $chart)) { Emit-ChartNode $k (Get-Prop $chart $k) "$ind`t" }
X "$ind</Settings>"
}
function Emit-Appearance {
param($el, [string]$indent, [string]$profile = 'field')
if ($null -eq $el) { return }
$order = switch ($profile) {
'decoration' { $script:appOrderDecoration }
'button' { $script:appOrderButton }
default { $script:appOrderField }
}
foreach ($key in $order) {
$val = Get-AppearanceValue -el $el -canonical $key
if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue }
$spec = $script:appearanceSpec[$key]
switch ($spec.kind) {
'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")</$($spec.tag)>" }
'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent }
'border' { Emit-BorderTag -val $val -indent $indent }
}
}
}
function Emit-Layout {
param($el, [string]$indent, [switch]$skipHeight, [bool]$multiLineDefault = $false)
# CommandSet (отключённые команды редактора) — общее свойство поля (input/label/check/
# spreadsheet/html/formatted/picture); в схеме рано (после TitleLocation, перед скалярами).
if ($el.excludedCommands -and @($el.excludedCommands).Count -gt 0) {
X "$indent<CommandSet>"
foreach ($cmd in $el.excludedCommands) { X "$indent`t<ExcludedCommand>$cmd</ExcludedCommand>" }
X "$indent</CommandSet>"
}
Emit-CommonElementProps -el $el -indent $indent
$amwExplicit = ($el.PSObject.Properties.Name -contains 'autoMaxWidth')
if ($amwExplicit) {
if ($el.autoMaxWidth -eq $false) { X "$indent<AutoMaxWidth>false</AutoMaxWidth>" }
} elseif ($multiLineDefault) {
X "$indent<AutoMaxWidth>false</AutoMaxWidth>"
}
if ($null -ne $el.maxWidth) { X "$indent<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$indent<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$indent<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$indent<Width>$($el.width)</Width>" }
if (-not $skipHeight -and $el.height) { X "$indent<Height>$($el.height)</Height>" }
if ($null -ne $el.horizontalStretch) { X "$indent<HorizontalStretch>$(if ($el.horizontalStretch){'true'}else{'false'})</HorizontalStretch>" }
if ($null -ne $el.verticalStretch) { X "$indent<VerticalStretch>$(if ($el.verticalStretch){'true'}else{'false'})</VerticalStretch>" }
if ($el.groupHorizontalAlign) { X "$indent<GroupHorizontalAlign>$($el.groupHorizontalAlign)</GroupHorizontalAlign>" }
if ($el.groupVerticalAlign) { X "$indent<GroupVerticalAlign>$($el.groupVerticalAlign)</GroupVerticalAlign>" }
if ($el.horizontalAlign) { X "$indent<HorizontalAlign>$($el.horizontalAlign)</HorizontalAlign>" }
Emit-GenericScalars -el $el -indent $indent
}
function Title-FromName {
param([string]$name)
if (-not $name) { return '' }
$s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2')
$s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2')
$parts = $s -split ' '
if ($parts.Count -eq 0) { return $s }
$out = New-Object System.Collections.ArrayList
[void]$out.Add($parts[0])
for ($i = 1; $i -lt $parts.Count; $i++) {
$p = $parts[$i]
if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) {
[void]$out.Add($p)
} else {
[void]$out.Add($p.ToLower())
}
}
return ($out -join ' ')
}
function Emit-Title {
# Нет ключа title → авто-вывод из имени (помощь модели).
# Явный title: "" (или null) → подавить (заголовок не эмитим).
# Явный непустой → эмитим как есть.
param($el, [string]$name, [string]$indent, [switch]$auto)
$hasKey = $null -ne $el.PSObject.Properties['title']
if ($hasKey) {
if ($el.title) { Emit-MLText -tag "Title" -text $el.title -indent $indent }
} elseif ($auto -and $name) {
Emit-MLText -tag "Title" -text (Title-FromName -name $name) -indent $indent
}
# ToolTip элемента (всплывающая подсказка) — по схеме сразу после Title.
if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent }
# ToolTipRepresentation — режим показа подсказки (None/Button/ShowBottom/…), после ToolTip.
if ($el.tooltipRepresentation) { X "$indent<ToolTipRepresentation>$($el.tooltipRepresentation)</ToolTipRepresentation>" }
}
function Map-TitleLoc {
param([string]$v)
switch ("$v".ToLower()) {
"none" { "None" }
"left" { "Left" }
"right" { "Right" }
"top" { "Top" }
"bottom" { "Bottom" }
"auto" { "Auto" }
default { "$v" }
}
}
# TitleLocation у check/radio: нет ключа → умный дефолт (Right/None), эмитится;
# "" → подавить (= дефолт платформы, она его сама не пишет); значение → эмитить (маппинг регистра).
function Emit-TitleLocation {
param($el, [string]$indent, [string]$smartDefault)
if ($null -ne $el.PSObject.Properties['titleLocation']) {
if ($el.titleLocation) { X "$indent<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
} elseif ($smartDefault) {
X "$indent<TitleLocation>$smartDefault</TitleLocation>"
}
}
function Warn-Unrecognized {
# drop-on-miss enum: значение не распознано → тег не эмитится. Громко, чтобы автор увидел потерю.
param([string]$key, $raw, [string[]]$valid, [string]$owner)
Write-Warning "Unrecognized $key '$raw' on '$owner'. Valid values: $($valid -join ', '). Value ignored."
}
function Emit-Group {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<UsualGroup name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
# Group orientation (направление). Legacy: group:'collapsible' = Vertical + behavior collapsible.
$groupVal = "$($el.group)".ToLower()
$orientation = switch ($groupVal) {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"alwayshorizontal" { "AlwaysHorizontal" }
"alwaysvertical" { "AlwaysVertical" }
"horizontalifpossible" { "HorizontalIfPossible" }
"collapsible" { "Vertical" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
elseif ($groupVal) { Warn-Unrecognized 'group orientation' $el.group @('vertical','horizontalIfPossible','alwaysHorizontal') $name }
# Behavior: ключ behavior (usual/collapsible/popup) → <Behavior>; отсутствие = Авто (не эмитим).
# Legacy: group:'collapsible' эквивалентно behavior:'collapsible'.
$behaviorVal = if ($el.behavior) { "$($el.behavior)".ToLower() } elseif ($groupVal -eq "collapsible") { "collapsible" } else { $null }
$bmap = @{ "usual"="Usual"; "collapsible"="Collapsible"; "popup"="PopUp" }
if ($behaviorVal -and $bmap.ContainsKey($behaviorVal)) {
X "$inner<Behavior>$($bmap[$behaviorVal])</Behavior>"
} elseif ($el.behavior -and -not $bmap.ContainsKey($behaviorVal)) {
Warn-Unrecognized 'behavior' $el.behavior @('collapsible','popup') $name
}
# Collapsed — у Collapsible и PopUp (не привязано к одному behavior)
if ($el.collapsed -eq $true) { X "$inner<Collapsed>true</Collapsed>" }
# Representation
if ($el.representation) {
$repr = switch ("$($el.representation)") {
"none" { "None" }
"normal" { "NormalSeparation" }
"weak" { "WeakSeparation" }
"strong" { "StrongSeparation" }
default { "$($el.representation)" }
}
X "$inner<Representation>$repr</Representation>"
}
# Использование текущей строки группы (после Representation, порядок XSD)
if ($el.currentRowUse) { X "$inner<CurrentRowUse>$($el.currentRowUse)</CurrentRowUse>" }
# ShowTitle
if ($null -ne $el.showTitle) { X "$inner<ShowTitle>$(if ($el.showTitle){'true'}else{'false'})</ShowTitle>" }
# Заголовок свёрнутого представления (collapsible/popup) — мультиязычный текст
if ($el.collapsedTitle) { Emit-MLText -tag "CollapsedRepresentationTitle" -text $el.collapsedTitle -indent $inner }
# United
if ($el.united -eq $false) { X "$inner<United>false</United>" }
# Формат значения пути к данным заголовка (<Format>; парный к titleDataPath группы)
if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner }
if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner }
Emit-CommonFlags -el $el -indent $inner
Emit-Layout -el $el -indent $inner
# Оформление (цвета/шрифты/граница) — перед компаньоном
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
}
X "$inner</ChildItems>"
}
X "$indent</UsualGroup>"
}
function Emit-ColumnGroup {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<ColumnGroup name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
# Group orientation (horizontal / vertical / inCell — последнее только здесь)
$groupVal = "$($el.columnGroup)"
$orientation = switch ($groupVal) {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"inCell" { "InCell" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
elseif ($groupVal) { Warn-Unrecognized 'columnGroup orientation' $el.columnGroup @('vertical','horizontal','inCell') $name }
if ($null -ne $el.showTitle) { X "$inner<ShowTitle>$(if ($el.showTitle){'true'}else{'false'})</ShowTitle>" }
# showInHeader эмитится общим Emit-CommonElementProps (через Emit-Layout)
Emit-CommonFlags -el $el -indent $inner
Emit-Layout -el $el -indent $inner
# Картинка заголовка колонки-группы (после ShowInHeader/Layout, перед оформлением — порядок XSD)
Emit-ColumnPics -el $el -indent $inner
# Оформление (цвета/шрифты/граница) — перед компаньоном
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
}
X "$inner</ChildItems>"
}
X "$indent</ColumnGroup>"
}
function Emit-Input {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<InputField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) {
$loc = switch ("$($el.titleLocation)") {
"none" { "None" }
"left" { "Left" }
"right" { "Right" }
"top" { "Top" }
"bottom" { "Bottom" }
default { "$($el.titleLocation)" }
}
X "$inner<TitleLocation>$loc</TitleLocation>"
}
if ($null -ne $el.multiLine) { X "$inner<MultiLine>$(if ($el.multiLine){'true'}else{'false'})</MultiLine>" }
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
# ChoiceButton — захват «как есть» (платформа эмитит явное значение; ref-поля выводят сама,
# декомпилятор фиксирует факт. значение). Нет ключа → не эмитим (не додумываем по событию).
if ($null -ne $el.choiceButton) { X "$inner<ChoiceButton>$(if ($el.choiceButton){'true'}else{'false'})</ChoiceButton>" }
# Кнопки поля ввода — захват «как есть» (платформа эмитит явное значение, в т.ч. false)
if ($null -ne $el.clearButton) { X "$inner<ClearButton>$(if ($el.clearButton){'true'}else{'false'})</ClearButton>" }
if ($null -ne $el.spinButton) { X "$inner<SpinButton>$(if ($el.spinButton){'true'}else{'false'})</SpinButton>" }
if ($null -ne $el.dropListButton) { X "$inner<DropListButton>$(if ($el.dropListButton){'true'}else{'false'})</DropListButton>" }
if ($null -ne $el.choiceListButton) { X "$inner<ChoiceListButton>$(if ($el.choiceListButton){'true'}else{'false'})</ChoiceListButton>" }
if ($null -ne $el.markIncomplete) { X "$inner<AutoMarkIncomplete>$(if ($el.markIncomplete){'true'}else{'false'})</AutoMarkIncomplete>" }
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
Emit-ColumnPics -el $el -indent $inner
if ($el.textEdit -eq $false) { X "$inner<TextEdit>false</TextEdit>" }
# InputField-специфичные скаляры (захват «как есть»: платформа эмитит явное не-дефолтное значение)
foreach ($p in @(
@('wrap','Wrap'), @('openButton','OpenButton'), @('listChoiceMode','ListChoiceMode'),
@('extendedEditMultipleValues','ExtendedEditMultipleValues'), @('chooseType','ChooseType'),
@('quickChoice','QuickChoice'), @('autoChoiceIncomplete','AutoChoiceIncomplete')
)) {
if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
}
# Ограничение доступных типов (поле на составном типе): домен типов + явный набор.
# availableTypes — формат типа реквизита (§type); Emit-Type сам разбирает мультитип "a | b".
if ($null -ne $el.typeDomainEnabled) { X "$inner<TypeDomainEnabled>$(if ($el.typeDomainEnabled){'true'}else{'false'})</TypeDomainEnabled>" }
if ($el.availableTypes) { Emit-Type -typeStr $el.availableTypes -indent $inner -tag 'AvailableTypes' }
# InputField-специфичные value-скаляры
foreach ($p in @(
@('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'),
@('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath')
)) {
if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")</$($p[1])>" }
}
# MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) {
if ($null -ne $el.($p[0])) {
$mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' }
X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")</$($p[1])>"
}
}
if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" }
Emit-PictureRef -val $el.choiceButtonPicture -picTag 'ChoiceButtonPicture' -indent $inner
Emit-Layout -el $el -indent $inner -multiLineDefault ([bool]($el.multiLine -eq $true))
if ($el.inputHint) {
Emit-MLText -tag "InputHint" -text $el.inputHint -indent $inner
}
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner }
if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner }
Emit-ChoiceList -el $el -indent $inner
# Связи по типу / связи параметров выбора / параметры выбора
Emit-TypeLink -el $el -indent $inner
Emit-ChoiceParameterLinks -el $el -indent $inner
Emit-ChoiceParameters -el $el -indent $inner
# Оформление (цвета/шрифты/граница) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "input"
X "$indent</InputField>"
}
function Emit-Check {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<CheckBoxField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
Emit-ColumnPics -el $el -indent $inner
# CheckBoxType: нет ключа → умный дефолт Auto; "" → подавить; значение → маппинг
if ($null -ne $el.PSObject.Properties['checkBoxType']) {
if ($el.checkBoxType) {
$cbt = switch ("$($el.checkBoxType)".ToLower()) { 'auto' {'Auto'} 'checkbox' {'CheckBox'} 'switcher' {'Switcher'} 'tumbler' {'Tumbler'} default {"$($el.checkBoxType)"} }
X "$inner<CheckBoxType>$cbt</CheckBoxType>"
}
} else { X "$inner<CheckBoxType>Auto</CheckBoxType>" }
Emit-TitleLocation -el $el -indent $inner -smartDefault "Right"
Emit-Layout -el $el -indent $inner
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner }
if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner }
# Оформление (цвета/шрифты/граница) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "check"
X "$indent</CheckBoxField>"
}
# Maps Russian/English root of a typed reference path to canonical English root.
# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y".
$script:refRootSynonyms = @{
"Перечисление" = "Enum"
"Справочник" = "Catalog"
"Документ" = "Document"
"ПланСчетов" = "ChartOfAccounts"
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
"ПланВидовРасчета" = "ChartOfCalculationTypes"
"ПланВидовРасчёта" = "ChartOfCalculationTypes"
"ПланОбмена" = "ExchangePlan"
"БизнесПроцесс" = "BusinessProcess"
"Задача" = "Task"
"РегистрСведений" = "InformationRegister"
"РегистрНакопления" = "AccumulationRegister"
"РегистрБухгалтерии" = "AccountingRegister"
"РегистрРасчета" = "CalculationRegister"
"РегистрРасчёта" = "CalculationRegister"
"ЖурналДокументов" = "DocumentJournal"
"КритерийОтбора" = "FilterCriterion"
}
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")
# Нормализация типа таблицы динсписка: "Справочник.Контрагенты" → "Catalog.Контрагенты".
# Прощающий ввод: принимаем рус-имя метаданных, переводим в платформенное. Уже англ — без изменений.
function Normalize-MetaTypeRef {
param([string]$ref)
if ([string]::IsNullOrEmpty($ref)) { return $ref }
$dot = $ref.IndexOf('.')
if ($dot -lt 1) { return $ref }
$root = $ref.Substring(0, $dot)
if ($script:refRootSynonyms.ContainsKey($root)) {
return $script:refRootSynonyms[$root] + $ref.Substring($dot)
}
return $ref
}
# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." }
function Normalize-ChoiceValue {
param($value)
# Booleans
if ($value -is [bool]) {
return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } }
}
# Numbers (int / decimal / double)
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
return @{ XsiType = "xs:decimal"; Text = "$value" }
}
$s = "$value"
if ([string]::IsNullOrEmpty($s)) {
return @{ XsiType = "xs:string"; Text = "" }
}
# ISO datetime ("2020-01-01T00:00:00") → xs:dateTime
if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') {
return @{ XsiType = "xs:dateTime"; Text = $s }
}
# Raw-ссылка по GUID (метаданные.значение, оба GUID): "GUID.GUID" → xr:DesignTimeRef
# (всегда ссылка, не строка; named-ссылки Enum.X.Y детектятся ниже).
if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F]{8}-[0-9a-fA-F-]+$') {
return @{ XsiType = "xr:DesignTimeRef"; Text = $s }
}
# Try to detect typed reference path: "<Root>.<Type>[.<Member>.<Value>]"
$parts = $s -split '\.'
if ($parts.Count -ge 2) {
$root = $parts[0]
$canonRoot = $null
if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] }
elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root }
if ($canonRoot) {
$typeName = $parts[1]
$normalized = $null
if ($canonRoot -eq "Enum") {
if ($parts.Count -eq 2) {
# "Enum.X" alone — not a value, treat as string
} elseif ($parts.Count -eq 3) {
# "Enum.X.Y" — insert .EnumValue. ("EmptyRef" — пустая ссылка, БЕЗ вставки)
if ($parts[2] -eq 'EmptyRef') { $normalized = "Enum.$typeName.EmptyRef" }
else { $normalized = "Enum.$typeName.EnumValue.$($parts[2])" }
} else {
# "Enum.X.<member>.Y..." — replace member with EnumValue (handles ЗначениеПеречисления too)
$member = $parts[2]
if ($script:enumValueSynonyms -contains $member) {
$rest = $parts[3..($parts.Count-1)] -join '.'
$normalized = "Enum.$typeName.EnumValue.$rest"
} else {
$rest = $parts[2..($parts.Count-1)] -join '.'
$normalized = "Enum.$typeName.EnumValue.$rest"
}
}
} else {
# Other ref roots: just translate root, keep tail as-is
if ($parts.Count -ge 3) {
$tail = $parts[1..($parts.Count-1)] -join '.'
$normalized = "$canonRoot.$tail"
}
}
if ($normalized) {
return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized }
}
}
}
return @{ XsiType = "xs:string"; Text = $s }
}
# Emit Presentation block for a choiceList item.
# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}.
# Empty/null → emits empty <Presentation/>.
function Emit-ChoicePresentation {
param($pres, [string]$indent)
if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) {
X "$indent<Presentation/>"
return
}
$pairs = @()
if ($pres -is [string]) {
$pairs += ,@("ru", $pres)
} elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) {
foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") }
} elseif ($pres.PSObject -and $pres.PSObject.Properties) {
foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") }
} else {
$pairs += ,@("ru", "$pres")
}
X "$indent<Presentation>"
foreach ($pair in $pairs) {
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>"
X "$indent`t</v8:item>"
}
X "$indent</Presentation>"
}
# <Value> для choiceList/choiceParameters: пустой текст → самозакрывающийся тег (зеркало платформы).
function Get-ChoiceValueTag {
param($norm)
if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" }
return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
}
# Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField.
# Элемент: { value, presentation?/title? } (+ рус. синонимы значение/представление).
function Emit-ChoiceList {
param($el, [string]$indent)
if (-not $el.choiceList -or $el.choiceList.Count -eq 0) { return }
X "$indent<ChoiceList>"
$itemIndent = "$indent`t"
foreach ($item in $el.choiceList) {
# value (+ рус. синоним "значение")
$valRaw = $null
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("value")) { $valRaw = $item["value"] }
elseif ($item.Contains("значение")) { $valRaw = $item["значение"] }
} else {
if ($item.PSObject.Properties["value"]) { $valRaw = $item.value }
elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" }
}
# presentation (presentation OR title синоним)
$presRaw = $null
$hasPres = $false
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true }
elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true }
elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true }
} else {
if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true }
elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true }
elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true }
}
# valueType: явный xsi:type значения (системное перечисление ent:*, иной не-примитив) —
# переопределяет авто-детект (Normalize-ChoiceValue вывела бы xs:string).
$vtRaw = $null
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("valueType")) { $vtRaw = "$($item["valueType"])" }
} elseif ($item.PSObject.Properties["valueType"]) { $vtRaw = "$($item.valueType)" }
if ($vtRaw -eq 'nil') { $norm = @{ XsiType = $null; Text = $null; Nil = $true } }
elseif ($vtRaw) { $norm = @{ XsiType = $vtRaw; Text = "$valRaw" } }
else { $norm = Normalize-ChoiceValue -value $valRaw }
# авто-вывод presentation, если не задан
if (-not $hasPres) {
if ($norm.XsiType -eq "xr:DesignTimeRef") {
$tail = ($norm.Text -split '\.')[-1]
$presRaw = Title-FromName -name $tail
} else {
$presRaw = $norm.Text
}
}
X "$itemIndent<xr:Item>"
$valIndent = "$itemIndent`t"
X "$valIndent<xr:Presentation/>"
X "$valIndent<xr:CheckState>0</xr:CheckState>"
X "$valIndent<xr:Value xsi:type=`"FormChoiceListDesTimeValue`">"
Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t"
X "$valIndent`t$(if ($norm.Nil) { '<Value xsi:nil="true"/>' } else { Get-ChoiceValueTag $norm })"
X "$valIndent</xr:Value>"
X "$itemIndent</xr:Item>"
}
X "$indent</ChoiceList>"
}
# Чтение свойства из hashtable/PSCustomObject по списку синонимов (первый найденный, иначе $null).
function Get-ElProp {
param($obj, [string[]]$names)
if ($null -eq $obj) { return $null }
foreach ($n in $names) {
if ($obj -is [System.Collections.IDictionary]) {
if ($obj.Contains($n)) { return $obj[$n] }
} elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) {
return $obj.PSObject.Properties[$n].Value
}
}
return $null
}
# Приведение строкового литерала shorthand к типу: true/false → bool, целое/дробное → число,
# иначе строка (ref-путь нормализует уже Normalize-ChoiceValue).
function ConvertTo-ScalarLiteral {
param([string]$s)
$t = "$s".Trim()
if ($t -match '^(?i:true)$') { return $true }
if ($t -match '^(?i:false)$') { return $false }
if ($t -match '^-?\d+$') { return [int]$t }
if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) }
return $t
}
# Shorthand параметра выбора: "name=value" либо "name=v1, v2, …" (запятые → массив). → {name, value}.
function ConvertFrom-ChoiceParamShorthand {
param([string]$s)
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$name = $s.Substring(0, $eq).Trim()
$rest = $s.Substring($eq + 1)
if ($rest -match ',') {
$vals = @()
foreach ($part in ($rest -split ',')) { $vals += ,(ConvertTo-ScalarLiteral $part) }
return @{ name = $name; value = $vals }
}
return @{ name = $name; value = (ConvertTo-ScalarLiteral $rest) }
}
# Shorthand связи параметров выбора: "name=dataPath" либо "name=dataPath:DontChange". → {name, dataPath, valueChange?}.
function ConvertFrom-ChoiceParamLinkShorthand {
param([string]$s)
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$o = @{ name = $s.Substring(0, $eq).Trim() }
$rest = $s.Substring($eq + 1).Trim()
if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') {
$o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2]
} else {
$o['dataPath'] = $rest
}
return $o
}
# Shorthand связи по типу: "dataPath" либо "dataPath#linkItem". → {dataPath, linkItem}.
function ConvertFrom-TypeLinkShorthand {
param([string]$s)
if ($s -match '^(.*)#(\d+)$') { return @{ dataPath = $matches[1].Trim(); linkItem = [int]$matches[2] } }
return @{ dataPath = "$s".Trim() }
}
# Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): <Presentation/> + <Value>.
# Скаляр → один Value (через Normalize-ChoiceValue); массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue.
function Emit-ChoiceParamValue {
# $isArray передаётся ЯВНО из вызывающего кода: PowerShell разворачивает одноэлементный массив
# при биндинге параметра ($value становится скаляром), поэтому определять массив тут — ненадёжно
# (1-элементный список `["X"]` эмитился бы скаляром вместо FixedArray). foreach по скаляру = 1 итерация.
param($value, [string]$indent, [bool]$isArray)
X "$indent<Presentation/>"
if ($isArray) {
X "$indent<Value xsi:type=`"v8:FixedArray`">"
foreach ($v in $value) {
$norm = Normalize-ChoiceValue -value $v
X "$indent`t<v8:Value xsi:type=`"FormChoiceListDesTimeValue`">"
X "$indent`t`t<Presentation/>"
X "$indent`t`t$(Get-ChoiceValueTag $norm)"
X "$indent`t</v8:Value>"
}
X "$indent</Value>"
} else {
$norm = Normalize-ChoiceValue -value $value
X "$indent$(Get-ChoiceValueTag $norm)"
}
}
# <ChoiceParameters> (параметры выбора поля ввода) — [{name, value}]. value через Normalize-ChoiceValue;
# массив значений → FixedArray. Рус. синонимы имя/значение.
function Emit-ChoiceParameters {
param($el, [string]$indent)
$cp = $el.choiceParameters
if (-not $cp -or @($cp).Count -eq 0) { return }
X "$indent<ChoiceParameters>"
foreach ($item in @($cp)) {
if ($item -is [string]) { $item = ConvertFrom-ChoiceParamShorthand $item }
$name = Get-ElProp $item @('name','имя')
# Наличие ключа value (≠ значения) + ПРЯМОЙ доступ к значению (без Get-ElProp): его return
# разворачивает 1-элементный массив (PS unwrap), теряя массив-ность → FixedArray не эмитится.
# Индексер/member-доступ массив сохраняет; if-выражение/функция-return — нет.
$hasVal = $false; $val = $null
if ($item -is [System.Collections.IDictionary]) {
if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] }
elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] }
} else {
if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value }
elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value }
}
$valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string])
X "$indent`t<app:item name=`"$(Esc-Xml "$name")`">"
# Параметр выбора без значения → <app:value xsi:nil="true"/> (платформа, 13 в корпусе);
# со значением (в т.ч. пустой строкой) → FormChoiceListDesTimeValue.
if (-not $hasVal) {
X "$indent`t`t<app:value xsi:nil=`"true`"/>"
} else {
X "$indent`t`t<app:value xsi:type=`"FormChoiceListDesTimeValue`">"
Emit-ChoiceParamValue -value $val -indent "$indent`t`t`t" -isArray $valIsArray
X "$indent`t`t</app:value>"
}
X "$indent`t</app:item>"
}
X "$indent</ChoiceParameters>"
}
# <ChoiceParameterLinks> (связи параметров выбора) — [{name, dataPath, valueChange?}].
# valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы.
function Emit-ChoiceParameterLinks {
param($el, [string]$indent)
$cpl = $el.choiceParameterLinks
if (-not $cpl -or @($cpl).Count -eq 0) { return }
X "$indent<ChoiceParameterLinks>"
foreach ($lk in @($cpl)) {
if ($lk -is [string]) { $lk = ConvertFrom-ChoiceParamLinkShorthand $lk }
$name = Get-ElProp $lk @('name','имя')
$dp = Get-ElProp $lk @('dataPath','path','путь')
$vcRaw = Get-ElProp $lk @('valueChange','режимИзменения')
$vc = "Clear"
if ($vcRaw) {
$vc = switch -Regex ("$vcRaw".ToLower()) {
'^(clear|очистить|очистка)$' { "Clear"; break }
'^(dontchange|неизменять|неменять|нет)$' { "DontChange"; break }
default { "$vcRaw" }
}
}
X "$indent`t<xr:Link>"
X "$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>"
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>"
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
X "$indent`t</xr:Link>"
}
X "$indent</ChoiceParameterLinks>"
}
# <TypeLink> (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0.
function Emit-TypeLink {
param($el, [string]$indent)
$tl = $el.typeLink
if (-not $tl) { return }
if ($tl -is [string]) { $tl = ConvertFrom-TypeLinkShorthand $tl }
$dp = Get-ElProp $tl @('dataPath','path','путь')
$li = Get-ElProp $tl @('linkItem','элементСвязи')
if ($null -eq $li) { $li = 0 }
X "$indent<TypeLink>"
X "$indent`t<xr:DataPath>$(Esc-Xml "$dp")</xr:DataPath>"
X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
X "$indent</TypeLink>"
}
function Emit-Radio {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<RadioButtonField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
Emit-TitleLocation -el $el -indent $inner -smartDefault "None"
# RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms.
$rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" }
$rbt = switch -Regex ($rbtRaw.ToLower()) {
'^(auto|авто)$' { "Auto"; break }
'^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break }
'^(tumbler|тумблер)$' { "Tumbler"; break }
default { $rbtRaw }
}
X "$inner<RadioButtonType>$rbt</RadioButtonType>"
if ($null -ne $el.columnsCount) {
X "$inner<ColumnsCount>$($el.columnsCount)</ColumnsCount>"
}
Emit-ChoiceList -el $el -indent $inner
Emit-Layout -el $el -indent $inner
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
# Оформление (цвета/шрифты/граница) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio"
X "$indent</RadioButtonField>"
}
# Заголовок декорации (Label/Picture): formatted-aware <Title> через единую ML-text форму
# (reuse Resolve-MLFormatted, как у extendedTooltip). Атрибут formatted эмитится ВСЕГДА
# (специфика декораций). Sibling-ключ `formatted` — back-compat override авто-детекта.
function Emit-DecorationTitle {
param($el, [string]$name, [string]$indent, [switch]$auto)
$hasKey = $null -ne $el.PSObject.Properties['title']
$titleVal = if ($hasKey) { $el.title } elseif ($auto -and $name) { Title-FromName -name $name } else { $null }
if ($titleVal) {
$r = Resolve-MLFormatted $titleVal
$fmt = if ($null -ne $el.PSObject.Properties['formatted']) { [bool]$el.formatted } else { $r.formatted }
X "$indent<Title formatted=`"$(if ($fmt) { 'true' } else { 'false' })`">"
Emit-MLItems -val $r.text -indent "$indent`t"
X "$indent</Title>"
}
if ($el.tooltip) { Emit-MLText -tag "ToolTip" -text $el.tooltip -indent $indent }
if ($el.tooltipRepresentation) { X "$indent<ToolTipRepresentation>$($el.tooltipRepresentation)</ToolTipRepresentation>" }
}
function Emit-Label {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<LabelDecoration name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
# Порядок как у платформы: own-content (флаги/hyperlink/layout/оформление) ПЕРЕД Title
# (корпус layout-first 16970 vs 44 — заодно убирает шум атрибуции харнесса на многострочном Title).
Emit-CommonFlags -el $el -indent $inner
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
Emit-Layout -el $el -indent $inner
Emit-Appearance -el $el -indent $inner -profile 'decoration'
Emit-DecorationTitle -el $el -name $name -indent $inner -auto
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "label"
X "$indent</LabelDecoration>"
}
function Emit-LabelField {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<LabelField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
# FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
# PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
Emit-ColumnPics -el $el -indent $inner
# ВНИМАНИЕ: у LabelField платформенный тег именно <Hiperlink> (опечатка 1С), не <Hyperlink>.
if ($el.hyperlink -eq $true) { X "$inner<Hiperlink>true</Hiperlink>" }
Emit-Layout -el $el -indent $inner
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner }
if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner }
# Оформление (цвета/шрифты/граница + header/footer) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField"
X "$indent</LabelField>"
}
# Блок свойств таблицы, привязанной к динамическому списку (Group A defaults + B/C).
# Платформа всегда эмитит этот блок на дин-список-таблице; компилятор зеркалит дефолты,
# DSL-ключ переопределяет; декомпилятор инвертирует (опускает значения = дефолту).
function Emit-DynListTableBlock {
param($el, [string]$indent)
# (useAlternationRowColor — общее свойство таблицы, эмитится в Emit-Table)
# Group A (гарант. блок, n=5079): дефолт + override
$ar = if ($el.autoRefresh -eq $true) { "true" } else { "false" }
X "$indent<AutoRefresh>$ar</AutoRefresh>"
$arp = if ($el.PSObject.Properties["autoRefreshPeriod"] -and $null -ne $el.autoRefreshPeriod) { $el.autoRefreshPeriod } else { 60 }
X "$indent<AutoRefreshPeriod>$arp</AutoRefreshPeriod>"
X "$indent<Period>"
X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</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</Period>"
$cfi = if ($el.choiceFoldersAndItems) { $el.choiceFoldersAndItems } else { "Items" }
X "$indent<ChoiceFoldersAndItems>$cfi</ChoiceFoldersAndItems>"
$rcr = if ($el.restoreCurrentRow -eq $true) { "true" } else { "false" }
X "$indent<RestoreCurrentRow>$rcr</RestoreCurrentRow>"
X "$indent<TopLevelParent xsi:nil=`"true`"/>"
$sr = if ($el.showRoot -eq $false) { "false" } else { "true" }
X "$indent<ShowRoot>$sr</ShowRoot>"
$arc = if ($el.allowRootChoice -eq $true) { "true" } else { "false" }
X "$indent<AllowRootChoice>$arc</AllowRootChoice>"
$uodc = if ($el.updateOnDataChange) { $el.updateOnDataChange } else { "Auto" }
X "$indent<UpdateOnDataChange>$uodc</UpdateOnDataChange>"
if ($el.userSettingsGroup) { X "$indent<UserSettingsGroup>$($el.userSettingsGroup)</UserSettingsGroup>" }
$agcru = if ($el.allowGettingCurrentRowURL -eq $false) { "false" } else { "true" }
X "$indent<AllowGettingCurrentRowURL>$agcru</AllowGettingCurrentRowURL>"
}
function Emit-Table {
param($el, [string]$name, [int]$id, [string]$indent)
$script:currentTableName = $name # дефолт source для кастомных дополнений в commandBar
X "$indent<Table name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.representation) {
X "$inner<Representation>$($el.representation)</Representation>"
}
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
# ChangeRowSet/Order — эмитим явное значение (в т.ч. false: платформа пишет его на ValueTable)
if ($el.PSObject.Properties['changeRowSet'] -and $null -ne $el.changeRowSet) {
X "$inner<ChangeRowSet>$(if ($el.changeRowSet -eq $true){'true'}else{'false'})</ChangeRowSet>"
}
if ($el.PSObject.Properties['changeRowOrder'] -and $null -ne $el.changeRowOrder) {
X "$inner<ChangeRowOrder>$(if ($el.changeRowOrder -eq $true){'true'}else{'false'})</ChangeRowOrder>"
}
if ($el.autoInsertNewRow -eq $true) { X "$inner<AutoInsertNewRow>true</AutoInsertNewRow>" }
# RowFilter — nil-плейсхолдер (всегда пустой); ключ присутствует → эмитим
if ($el.PSObject.Properties['rowFilter']) { X "$inner<RowFilter xsi:nil=`"true`"/>" }
# Высота в строках таблицы (<HeightInTableRows>) — отдельное свойство от <Height> (высота элемента,
# эмитится generic-ом Emit-Layout ниже). Таблица может нести оба (237 в корпусе).
if ($el.heightInTableRows) { X "$inner<HeightInTableRows>$($el.heightInTableRows)</HeightInTableRows>" }
if ($el.header -eq $false) { X "$inner<Header>false</Header>" }
if ($el.footer -eq $true) { X "$inner<Footer>true</Footer>" }
if ($el.commandBarLocation) {
X "$inner<CommandBarLocation>$($el.commandBarLocation)</CommandBarLocation>"
}
if ($el.searchStringLocation) {
X "$inner<SearchStringLocation>$($el.searchStringLocation)</SearchStringLocation>"
}
if ($el.choiceMode -eq $true) { X "$inner<ChoiceMode>true</ChoiceMode>" }
# Скаляры таблицы (захват «как есть»). Autofill — СВОЁ свойство таблицы (≠ AutoCommandBar autofill = tableAutofill).
if ($null -ne $el.autofill) { X "$inner<Autofill>$(if ($el.autofill){'true'}else{'false'})</Autofill>" }
if ($el.multipleChoice -eq $true) { X "$inner<MultipleChoice>true</MultipleChoice>" }
if ($el.searchOnInput) { X "$inner<SearchOnInput>$($el.searchOnInput)</SearchOnInput>" }
if ($null -ne $el.markIncomplete) { X "$inner<AutoMarkIncomplete>$(if ($el.markIncomplete){'true'}else{'false'})</AutoMarkIncomplete>" }
# Высота шапки/подвала в строках (pass-through; 1С толерантна к порядку детей Table)
if ($null -ne $el.headerHeight) { X "$inner<HeaderHeight>$($el.headerHeight)</HeaderHeight>" }
if ($null -ne $el.footerHeight) { X "$inner<FooterHeight>$($el.footerHeight)</FooterHeight>" }
if ($el.useAlternationRowColor -eq $true) { X "$inner<UseAlternationRowColor>true</UseAlternationRowColor>" }
if ($el.selectionMode) { X "$inner<SelectionMode>$($el.selectionMode)</SelectionMode>" }
if ($el.rowSelectionMode) { X "$inner<RowSelectionMode>$($el.rowSelectionMode)</RowSelectionMode>" }
if ($el.verticalLines -eq $false) { X "$inner<VerticalLines>false</VerticalLines>" }
if ($el.horizontalLines -eq $false) { X "$inner<HorizontalLines>false</HorizontalLines>" }
if ($el.initialTreeView) { X "$inner<InitialTreeView>$($el.initialTreeView)</InitialTreeView>" }
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
if ($el.rowPictureDataPath) { X "$inner<RowPictureDataPath>$($el.rowPictureDataPath)</RowPictureDataPath>" }
# RowsPicture — та же конвенция, что ValuesPicture (дефолт LoadTransparent=false; abs/TransparentPixel)
Emit-PictureRef -val $el.rowsPicture -picTag 'RowsPicture' -indent $inner
# Использование текущей строки таблицы (pass-through; в корпусе соседствует с блоком дин-списка)
if ($el.currentRowUse) { X "$inner<CurrentRowUse>$($el.currentRowUse)</CurrentRowUse>" }
# Запрос обновления дин-списка (pass-through; в корпусе всегда PullFromTop)
if ($el.refreshRequest) { X "$inner<RefreshRequest>$($el.refreshRequest)</RefreshRequest>" }
# Блок свойств дин-список-таблицы (помечена эвристикой 11b.4)
if ($el.PSObject.Properties["_dynList"] -and $el._dynList) { Emit-DynListTableBlock -el $el -indent $inner }
if ($el.viewStatusLocation) { X "$inner<ViewStatusLocation>$($el.viewStatusLocation)</ViewStatusLocation>" }
if ($el.searchControlLocation) { X "$inner<SearchControlLocation>$($el.searchControlLocation)</SearchControlLocation>" }
Emit-Layout -el $el -indent $inner
# CommandSet таблицы эмитится через Emit-Layout (общий механизм поля)
# Оформление (цвета/граница таблицы) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
# AutoCommandBar: приоритет commandBar-свойства (контент); иначе tableAutofill-shorthand; иначе пусто.
if ($null -ne $el.commandBar) {
Emit-CompanionPanel -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner -panel $el.commandBar
} elseif ($null -ne $el.tableAutofill) {
$acbId = New-Id
X "$inner<AutoCommandBar name=`"${name}КоманднаяПанель`" id=`"$acbId`">"
$afVal = if ($el.tableAutofill) { "true" } else { "false" }
X "$inner`t<Autofill>$afVal</Autofill>"
X "$inner</AutoCommandBar>"
} else {
Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner
}
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
$adds = $el.additions
Emit-TableAddition -typeKey 'searchString' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchString')
Emit-TableAddition -typeKey 'viewStatus' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'viewStatus')
Emit-TableAddition -typeKey 'searchControl' -tableName $name -indent $inner -override (Get-AdditionOverride $adds 'searchControl')
# Columns
if ($el.columns -and $el.columns.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($col in $el.columns) {
Emit-Element -el $col -indent "$inner`t"
}
X "$inner</ChildItems>"
}
Emit-Events -el $el -elementName $name -indent $inner -typeKey "table"
X "$indent</Table>"
}
function Emit-Pages {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<Pages name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
if ($el.pagesRepresentation) {
X "$inner<PagesRepresentation>$($el.pagesRepresentation)</PagesRepresentation>"
}
# Использование текущей строки (после PagesRepresentation, порядок XSD)
if ($el.currentRowUse) { X "$inner<CurrentRowUse>$($el.currentRowUse)</CurrentRowUse>" }
Emit-CommonFlags -el $el -indent $inner
Emit-Layout -el $el -indent $inner
# Оформление (цвета/шрифты/граница) заголовка группы страниц — TitleFont/TitleTextColor/… (как у Page)
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companion
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages"
# Children (pages)
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
}
X "$inner</ChildItems>"
}
X "$indent</Pages>"
}
function Emit-Page {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<Page name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
# Картинка страницы (иконка вкладки): после Title/флагов, перед Group (порядок XSD).
# Конвенция как у ValuesPicture (дефолт LoadTransparent=false): скаляр-Ref/'abs:X' или объект.
Emit-PictureRef -val $el.picture -picTag 'Picture' -indent $inner
if ($el.group) {
# Доступные значения страницы/обычной группы: Vertical / HorizontalIfPossible / AlwaysHorizontal
# (InCell — только у columnGroup). Horizontal/AlwaysVertical оставлены forgiving (legacy).
$orientation = switch ("$($el.group)") {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"alwaysHorizontal" { "AlwaysHorizontal" }
"alwaysVertical" { "AlwaysVertical" }
"horizontalIfPossible" { "HorizontalIfPossible" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
else { Warn-Unrecognized 'page group orientation' $el.group @('vertical','horizontalIfPossible','alwaysHorizontal') $name }
}
if ($null -ne $el.showTitle) { X "$inner<ShowTitle>$(if ($el.showTitle){'true'}else{'false'})</ShowTitle>" }
# Формат значения пути к данным заголовка (<Format>; парный к titleDataPath страницы)
if ($el.format) { Emit-MLText -tag "Format" -text $el.format -indent $inner }
if ($el.editFormat) { Emit-MLText -tag "EditFormat" -text $el.editFormat -indent $inner }
Emit-Layout -el $el -indent $inner
# Оформление страницы (BackColor / TitleTextColor / TitleFont) — после ShowTitle, перед компаньоном
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companion
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
}
X "$inner</ChildItems>"
}
X "$indent</Page>"
}
function Emit-Button {
param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false)
X "$indent<Button name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
# (общие свойства — через Emit-Layout ниже; отдельный вызов был бы двойной эмиссией)
# Type — context-aware:
# Inside command bar (cmdBar/autoCmdBar/popup) only CommandBarButton/CommandBarHyperlink are valid.
# UsualButton/Hyperlink would be silently ignored by 1C.
$btnType = $null
if ($el.type) {
$rawType = "$($el.type)"
if ($inCmdBar) {
# Be forgiving: any "ordinary button" hint resolves to CommandBarButton,
# any "hyperlink" hint resolves to CommandBarHyperlink. The model can pass
# either DSL ("usual"/"hyperlink") or XML names — all map to the right kind.
switch ($rawType) {
"usual" { $btnType = "CommandBarButton" }
"UsualButton" { $btnType = "CommandBarButton" }
"commandBar" { $btnType = "CommandBarButton" }
"CommandBarButton" { $btnType = "CommandBarButton" }
"hyperlink" { $btnType = "CommandBarHyperlink" }
"Hyperlink" { $btnType = "CommandBarHyperlink" }
"CommandBarHyperlink" { $btnType = "CommandBarHyperlink" }
default { $btnType = $rawType }
}
} else {
# Symmetric: any "ordinary button" hint → UsualButton, any "hyperlink" → Hyperlink.
switch ($rawType) {
"usual" { $btnType = "UsualButton" }
"UsualButton" { $btnType = "UsualButton" }
"commandBar" { $btnType = "UsualButton" }
"CommandBarButton" { $btnType = "UsualButton" }
"hyperlink" { $btnType = "Hyperlink" }
"Hyperlink" { $btnType = "Hyperlink" }
"CommandBarHyperlink" { $btnType = "Hyperlink" }
default { $btnType = $rawType }
}
}
} elseif ($inCmdBar) {
$btnType = "CommandBarButton"
}
if ($btnType) {
X "$inner<Type>$btnType</Type>"
}
# CommandName
if ($el.command) {
X "$inner<CommandName>Form.Command.$($el.command)</CommandName>"
}
# commandName — глобальная команда «как есть» (CommonCommand.X, Catalog.X.Command.Y …), без обёртки Form.
if ($el.commandName -and -not $el.command) {
X "$inner<CommandName>$($el.commandName)</CommandName>"
}
if ($el.stdCommand) {
$sc = "$($el.stdCommand)"
if ($sc -match '^(.+)\.(.+)$') {
X "$inner<CommandName>Form.Item.$($Matches[1]).StandardCommand.$($Matches[2])</CommandName>"
} else {
X "$inner<CommandName>Form.StandardCommand.$sc</CommandName>"
}
}
# Parameter команды (после CommandName): строка → xr:MDObjectRef (объект метаданных);
# объект {type} → v8:TypeDescription (грамматика типа). Forgiving-синоним 'параметр'.
$btnParam = if ($null -ne $el.PSObject.Properties['parameter']) { $el.parameter } elseif ($null -ne $el.PSObject.Properties['параметр']) { $el.параметр } else { $null }
if ($null -ne $btnParam) {
if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) {
Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"'
} else {
X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-Xml "$btnParam")</Parameter>"
}
}
# DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле)
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
$btnAuto = -not ($el.command -or $el.commandName -or $el.stdCommand)
Emit-Title -el $el -name $name -indent $inner -auto:$btnAuto
Emit-CommonFlags -el $el -indent $inner
if ($el.defaultButton -eq $true) { X "$inner<DefaultButton>true</DefaultButton>" }
# Check (пометка toggle-кнопки командной панели) — платформа эмитит только true.
# Ключ 'checked' (не 'check': 'check' — тип-ключ CheckBoxField, был бы конфликт диспетчера типов)
if ($el.checked -eq $true) { X "$inner<Check>true</Check>" }
# Picture
Emit-CommandPicture -pic $el.picture -elemLt $el.loadTransparent -indent $inner
if ($el.representation) {
X "$inner<Representation>$($el.representation)</Representation>"
}
if ($el.locationInCommandBar) {
X "$inner<LocationInCommandBar>$($el.locationInCommandBar)</LocationInCommandBar>"
}
Emit-Layout -el $el -indent $inner
# Оформление (цвета/шрифт/граница) — перед компаньоном (профиль кнопки)
Emit-Appearance -el $el -indent $inner -profile 'button'
# Companion
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "button"
X "$indent</Button>"
}
function Emit-PictureDecoration {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<PictureDecoration name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-DecorationTitle -el $el -name $name -indent $inner
# Текст при невыбранной картинке (NonselectedPictureText) — после Title (порядок корпуса)
if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner }
Emit-CommonFlags -el $el -indent $inner
# Источник картинки — ТОЛЬКО $el.src (у PictureDecoration ключ 'picture' = тип/имя элемента, не источник).
# Префикс "abs:" → встроенная картинка <xr:Abs>; иначе именованная/стилевая <xr:Ref>.
if ($el.src) {
$srcStr = "$($el.src)"
$lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" }
X "$inner<Picture>"
if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
else { X "$inner`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>"
if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" }
X "$inner</Picture>"
}
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
Emit-Layout -el $el -indent $inner
# EnableDrag — фактическое значение (декорация-картинка перетаскиваема; декомпилятор ловит generic-ом)
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
# Оформление (цвета/шрифт/граница) — профиль декорации (1С толерантна к порядку appearance)
Emit-Appearance -el $el -indent $inner -profile 'decoration'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture"
X "$indent</PictureDecoration>"
}
function Emit-PictureField {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<PictureField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-CommonFlags -el $el -indent $inner
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
Emit-ColumnPics -el $el -indent $inner
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
Emit-Layout -el $el -indent $inner
# EnableDrag — фактическое значение (поле картинки перетаскиваемо; декомпилятор ловит generic-ом)
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
# ValuesPicture — picture (collection) used to render the field's value.
# Required for a Boolean-bound PictureField to actually show an icon.
# Скаляр (Ref) или объект {src, loadTransparent}; LoadTransparent эмитится всегда.
Emit-PictureRef -val $el.valuesPicture -picTag 'ValuesPicture' -indent $inner
if ($null -ne $el.nonselectedPictureText) { Emit-MLText -tag "NonselectedPictureText" -text $el.nonselectedPictureText -indent $inner }
# Оформление (цвета/шрифты/граница) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField"
X "$indent</PictureField>"
}
function Emit-Calendar {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<CalendarField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) {
$loc = switch ("$($el.titleLocation)") {
"none" { "None" }
"left" { "Left" }
"right" { "Right" }
"top" { "Top" }
"bottom" { "Bottom" }
"auto" { "Auto" }
default { "$($el.titleLocation)" }
}
X "$inner<TitleLocation>$loc</TitleLocation>"
}
Emit-Layout -el $el -indent $inner
# Календарно-специфичные свойства (порядок схемы: после layout, до companions)
if ($el.selectionMode) { X "$inner<SelectionMode>$($el.selectionMode)</SelectionMode>" }
if ($null -ne $el.showCurrentDate) { $v = if ($el.showCurrentDate) { "true" } else { "false" }; X "$inner<ShowCurrentDate>$v</ShowCurrentDate>" }
if ($null -ne $el.widthInMonths) { X "$inner<WidthInMonths>$($el.widthInMonths)</WidthInMonths>" }
if ($null -ne $el.heightInMonths) { X "$inner<HeightInMonths>$($el.heightInMonths)</HeightInMonths>" }
if ($null -ne $el.showMonthsPanel) { $v = if ($el.showMonthsPanel) { "true" } else { "false" }; X "$inner<ShowMonthsPanel>$v</ShowMonthsPanel>" }
# Оформление (цвета/шрифты/граница) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar"
X "$indent</CalendarField>"
}
# Спец-поля «документ/датчик» (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar):
# единый скелет поля (path/title/flags/titleLocation/editMode/layout/companions/events).
# Типоспец. enum/bool скаляры — через generic (Emit-Layout→Emit-GenericScalars);
# числовые скаляры датчиков (min/max/шаги) — без xsi:type (≠ типизированных InputField).
# enableDrag/enableStartDrag — общие (Emit-CommonElementProps), фактическое значение.
function Emit-SimpleField {
param($el, [string]$name, [int]$id, [string]$indent, [string]$xmlTag, [string]$typeKey)
X "$indent<$xmlTag name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
Emit-Layout -el $el -indent $inner
# EnableDrag — фактическое значение (SpreadSheet; платформа эмитит явный false). enableStartDrag — через Emit-Layout.
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
# Датчики (ProgressBar/TrackBar) — числовые скаляры (без xsi:type)
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'), @('largeStep','LargeStep'), @('markingStep','MarkingStep'), @('step','Step'))) {
if ($null -ne $el.($p[0])) { X "$inner<$($p[1])>$($el.($p[0]))</$($p[1])>" }
}
# Оформление (цвета/шрифты/граница) — перед компаньонами
Emit-Appearance -el $el -indent $inner -profile 'field'
# Companions
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
Emit-Events -el $el -elementName $name -indent $inner -typeKey $typeKey
X "$indent</$xmlTag>"
}
# GanttChartField — скелет поля + вложенная <Table> (полноценная таблица, через Emit-Element).
# Порядок (по корпусу): path/title/flags/titleLocation/layout/appearance, companions, Table, events.
function Emit-GanttChart {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<GanttChartField name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
Emit-Layout -el $el -indent $inner
Emit-Appearance -el $el -indent $inner -profile 'field'
Emit-CompanionPanel -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner -panel $el.contextMenu
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Вложенная таблица диаграммы Ганта (стандартный Table — переиспользуем Emit-Element)
if ($el.ganttTable) { Emit-Element -el $el.ganttTable -indent $inner }
Emit-Events -el $el -elementName $name -indent $inner -typeKey "ganttChart"
X "$indent</GanttChartField>"
}
function Emit-CommandBar {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<CommandBar name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
if ($el.commandSource) { X "$inner<CommandSource>$($el.commandSource)</CommandSource>" }
if ($el.autofill -eq $true) { X "$inner<Autofill>true</Autofill>" }
# CommandBar хранит HorizontalLocation фактически (включая Auto — декомпилятор ловит только при наличии);
# ≠ дополнениям, где Auto = умолчание-скип (Get-HLocation).
if ($el.horizontalLocation) {
$hlv = switch ("$($el.horizontalLocation)".ToLower()) { 'auto' {'Auto'} 'left' {'Left'} 'right' {'Right'} 'center' {'Center'} default {"$($el.horizontalLocation)"} }
X "$inner<HorizontalLocation>$hlv</HorizontalLocation>"
}
Emit-CommonFlags -el $el -indent $inner
Emit-Layout -el $el -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
X "$indent</CommandBar>"
}
function Emit-ButtonGroup {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<ButtonGroup name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
if ($el.commandSource) { X "$inner<CommandSource>$($el.commandSource)</CommandSource>" }
if ($el.representation) {
X "$inner<Representation>$($el.representation)</Representation>"
}
Emit-CommonFlags -el $el -indent $inner
Emit-Layout -el $el -indent $inner
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Children (кнопки в контексте командной панели)
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
X "$indent</ButtonGroup>"
}
function Emit-Popup {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<Popup name=`"$name`" id=`"$id`"$(DI-Attr $el)>"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
# Источник команд попапа (после Title/ToolTip, перед компаньоном) — как у ButtonGroup/CommandBar
if ($el.commandSource) { X "$inner<CommandSource>$($el.commandSource)</CommandSource>" }
Emit-CommandPicture -pic $el.picture -elemLt $el.loadTransparent -indent $inner
if ($el.representation) {
X "$inner<Representation>$($el.representation)</Representation>"
}
Emit-Layout -el $el -indent $inner
# Оформление попапа (TitleTextColor / TitleFont) — перед компаньоном
Emit-Appearance -el $el -indent $inner -profile 'field'
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner -content $el.extendedTooltip
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
X "$indent</Popup>"
}
# --- 8. Attribute emitter ---
# <FunctionalOptions><Item>FunctionalOption.X</Item>…</FunctionalOptions> — у Attribute/Command/Column.
# DSL: массив строк. Forgiving: "X" / "FunctionalOption.X" → FunctionalOption.X; GUID (расширение) — как есть.
function Emit-FunctionalOptions {
param($fo, [string]$indent)
if (-not $fo -or @($fo).Count -eq 0) { return }
X "$indent<FunctionalOptions>"
foreach ($opt in @($fo)) {
$v = "$opt"
if ($v -match '^[0-9a-fA-F]{8}-[0-9a-fA-F-]{27,}$') { } # GUID — как есть
elseif ($v -match '^FunctionalOption\.') { } # уже с префиксом
else { $v = "FunctionalOption.$v" }
X "$indent`t<Item>$v</Item>"
}
X "$indent</FunctionalOptions>"
}
# Колонка реквизита (ValueTable/Tree или AdditionalColumns): name/Title/Type/FunctionalOptions.
function Emit-AttrColumn {
param($col, [string]$indent)
$colId = New-Id
X "$indent<Column name=`"$($col.name)`" id=`"$colId`">"
if ($col.title) { Emit-MLText -tag "Title" -text $col.title -indent "$indent`t" }
Emit-Type -typeStr "$($col.type)" -indent "$indent`t"
# Проверка заполнения колонки → <FillCheck> (как у реквизита; bool true→ShowError / строка verbatim)
$cfcRaw = if ($null -ne $col.PSObject.Properties['fillCheck']) { $col.fillCheck } elseif ($null -ne $col.PSObject.Properties['fillChecking']) { $col.fillChecking } else { $null }
if ($null -ne $cfcRaw) { $cfcv = if ($cfcRaw -is [bool]) { if ($cfcRaw) { 'ShowError' } else { $null } } else { "$cfcRaw" }; if ($cfcv) { X "$indent`t<FillCheck>$cfcv</FillCheck>" } }
Emit-FunctionalOptions -fo $col.functionalOptions -indent "$indent`t"
# Ролевой доступ колонки (View/Edit) — xr-флаг, как у самого реквизита
if ($null -ne $col.view) { Emit-XrFlag -tag 'View' -val $col.view -indent "$indent`t" }
if ($null -ne $col.edit) { Emit-XrFlag -tag 'Edit' -val $col.edit -indent "$indent`t" }
X "$indent</Column>"
}
# --- Schema-параметры динамического списка (DataCompositionSchemaParameter) ---
# Та же сущность, что параметры СКД (см. skd-compile), но в форме: обёртка <Parameter>
# + дети с префиксом dcssch:. DSL переиспользует грамматику параметров СКД (shorthand +
# объект). Контекстные дефолты дин-списка (паттерн «умный дефолт у всегда-эмитируемого тега»):
# useRestriction — эмитим ВСЕГДА, дефолт TRUE (в СКД дефолт false);
# title — авто из имени (Title-FromName), если ключ не задан.
# Канон. порядок детей (по корпусу acc/erp 8.3.24): name, title, valueType, value,
# useRestriction, expression, availableValue*, valueListAllowed, availableAsField,
# inputParameters, denyIncompleteValues, use.
# Многоязычный текст в DCS-контексте — с xsi:type="v8:LocalStringType" (form-compile
# Emit-MLText его НЕ ставит, т.к. формовые <Title> голые; в dcssch:* — обязателен).
function Emit-DLMLText {
param([string]$tag, $text, [string]$indent)
X "$indent<$tag xsi:type=`"v8:LocalStringType`">"
Emit-MLItems -val $text -indent "$indent`t"
X "$indent</$tag>"
}
function Has-DLProp {
param($obj, [string]$name)
if ($null -eq $obj) { return $false }
if ($obj -is [System.Collections.IDictionary]) { return $obj.Contains($name) }
if ($obj.PSObject -and $obj.PSObject.Properties[$name]) { return $true }
return $false
}
function Split-DLValueListCsv {
param([string]$s)
$result = @()
if ($null -eq $s) { return ,$result }
$items = @(); $buf = New-Object System.Text.StringBuilder; $inQuote = $null
for ($i = 0; $i -lt $s.Length; $i++) {
$ch = $s[$i]
if ($inQuote) { [void]$buf.Append($ch); if ($ch -eq $inQuote) { $inQuote = $null } }
elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch; [void]$buf.Append($ch) }
elseif ($ch -eq ',') { $items += $buf.ToString(); [void]$buf.Clear() }
else { [void]$buf.Append($ch) }
}
if ($buf.Length -gt 0) { $items += $buf.ToString() }
foreach ($raw in $items) {
$t = $raw.Trim()
if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) }
if ($t -ne "") { $result += $t }
}
return ,$result
}
# Shorthand: "Имя [Заголовок]: Тип = Значение @valueList @hidden"
function Parse-DLParamShorthand {
param([string]$s)
$result = @{ name = ""; type = ""; value = $null; title = $null }
if ($s -match '@valueList') { $result.valueListAllowed = $true; $s = $s -replace '\s*@valueList', '' }
if ($s -match '@hidden') { $result.hidden = $true; $s = $s -replace '\s*@hidden', '' }
if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim(); $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() }
# Тип может быть СОСТАВНЫМ (A | B | C — с пробелами); значение — после '=' (тип '=' не содержит).
if ($s -match '^([^:]+):\s*([^=]+?)(\s*=\s*(.*))?$') {
$result.name = $Matches[1].Trim()
$typeRaw = $Matches[2].Trim()
if ($typeRaw -match '[|+]') {
$result.type = (($typeRaw -split '\s*[|+]\s*') | ForEach-Object { Resolve-TypeStr ($_.Trim()) }) -join ' | '
} else {
$result.type = Resolve-TypeStr $typeRaw
}
if ($Matches[4]) {
$rhs = $Matches[4].Trim()
$items = Split-DLValueListCsv $rhs
if ($items.Count -ge 2) { $result.value = $items; $result.valueListAllowed = $true }
elseif ($items.Count -eq 1) { $result.value = $items[0] }
else { $result.value = $rhs }
}
} else { $result.name = $s.Trim() }
return $result
}
function Test-DLEmptyValue {
param($v)
if ($null -eq $v) { return $true }
$s = "$v".Trim()
if ($s -eq "" -or $s -eq "_" -or $s.ToLowerInvariant() -eq "null") { return $true }
return $false
}
# Эмиссия <dcssch:value> по типу/значению (xs:* или dcscor:DesignTimeValue для ссылок).
function Emit-DLValue {
param([string]$type, $val, [string]$indent, [bool]$valueListAllowed = $false)
if (Test-DLEmptyValue $val) {
# Дин-список: пустое значение платформа ВСЕГДА пишет как xsi:nil, даже при известном
# типе (в отличие от типизированного пустого в параметрах отчёта СКД).
if ($valueListAllowed) { return }
X "$indent<dcssch:value xsi:nil=`"true`"/>"
return
}
$valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" }
if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-Xml $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type
elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" }
else {
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" }
elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" }
else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" }
}
}
# <dcssch:valueType> — обёртка + тело через Emit-SingleType (ref-типы → cfg:, как в форме).
function Emit-DLValueType {
param($typeStr, [string]$indent)
if (-not $typeStr) { return }
X "$indent<dcssch:valueType>"
$parts = "$typeStr" -split '\s*[|+]\s*'
foreach ($part in $parts) { Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" }
X "$indent</dcssch:valueType>"
}
function Emit-DLAvailableValue {
param($av, [string]$type, [string]$indent)
X "$indent<dcssch:availableValue>"
$avVal = if (Has-DLProp $av 'value') { $av.value } else { $null }
Emit-DLValue -type $type -val $avVal -indent "$indent`t" -valueListAllowed $false
$pres = if ($av.presentation) { $av.presentation } elseif ($av.title) { $av.title } else { $null }
if ($pres) { Emit-DLMLText -tag "dcssch:presentation" -text $pres -indent "$indent`t" }
X "$indent</dcssch:availableValue>"
}
# <dcssch:inputParameters> — ChoiceParameters / ChoiceParameterLinks / простое значение (порт из skd).
function Emit-DLInputParameters {
param($ip, [string]$indent)
if ($null -eq $ip) { return }
$items = @($ip)
if ($items.Count -eq 0) { return }
X "$indent<dcssch:inputParameters>"
foreach ($item in $items) {
X "$indent`t<dcscor:item>"
if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>"
if (Has-DLProp $item 'choiceParameters') {
$cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() }
if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" }
else {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">"
foreach ($cpItem in $cpItems) {
X "$indent`t`t`t<dcscor:item>"
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>"
foreach ($v in @($cpItem.values)) {
if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" }
elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" }
else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" }
}
X "$indent`t`t`t</dcscor:item>"
}
X "$indent`t`t</dcscor:value>"
}
} elseif (Has-DLProp $item 'choiceParameterLinks') {
$cplItems = if ($null -ne $item.choiceParameterLinks) { @($item.choiceParameterLinks) } else { @() }
if ($cplItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`"/>" }
else {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">"
foreach ($cplItem in $cplItems) {
X "$indent`t`t`t<dcscor:item>"
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>"
X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>"
$mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' }
X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>"
X "$indent`t`t`t</dcscor:item>"
}
X "$indent`t`t</dcscor:value>"
}
} elseif (Has-DLProp $item 'typeLink') {
# Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра).
$tl = $item.typeLink
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">"
$tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-Xml "$tlf")</dcscor:field>" }
$tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-Xml "$tli")</dcscor:linkItem>" }
X "$indent`t`t</dcscor:value>"
} elseif (Has-DLProp $item 'value') {
$val = $item.value
if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" }
elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" }
elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" }
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" }
}
X "$indent`t</dcscor:item>"
}
X "$indent</dcssch:inputParameters>"
}
# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd-compile ──
# Грамматика идентична СКД: shorthand "Имя = Значение @off @user" или объект
# {parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation?}.
function Test-EmptyValue {
param($v)
if ($null -eq $v) { return $true }
$s = "$v".Trim()
if ($s -eq "") { return $true }
if ($s -eq "_") { return $true }
if ($s.ToLowerInvariant() -eq "null") { return $true }
return $false
}
function Emit-EmptyValue {
param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false)
if ($valueListAllowed) { return }
$t = if ($null -eq $type) { "" } else { "$type" }
$tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t }
$pf = $tagPrefix
if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" }
elseif ($t -eq "StandardPeriod") {
X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">Custom</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</${pf}value>"
}
elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" }
elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>" }
elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>" }
elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false</${pf}value>" }
else { X "$indent<${pf}value xsi:nil=`"true`"/>" }
}
function Parse-DataParamShorthand {
param([string]$s)
$result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null }
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()
if ($s -match '^([^=]+)=\s*(.+)$') {
$result.parameter = $Matches[1].Trim()
$valStr = $Matches[2].Trim()
$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
}
function Emit-DataParameters {
param($items, [string]$indent, $blockViewMode = $null)
if (-not $items -or @($items).Count -eq 0) { return }
X "$indent<dcsset:dataParameters>"
foreach ($dp in @($items)) {
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>"
$dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string])
if ($dpValIsArr) {
# Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое.
$avtype = "$($dp.valueType)"
foreach ($v in @($dp.value)) {
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" }
if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-Xml $vStr)</dcscor:value>" }
elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $vStr)</dcscor:value>" }
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $vStr)</dcscor:value>" }
}
} elseif ($dp.nilValue -eq $true) {
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
} elseif ((Test-EmptyValue $dp.value) -and $dp.valueType) {
# Явный типизированный пустой (xs:string-плейсхолдер и т.п.)
Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false
} elseif (Test-EmptyValue $dp.value) {
# Нет значения и нет valueType → НЕ эмитим value-узел (form дин-список: use=false плейсхолдер).
# (В отличие от skd-settings, где значение всегда присутствует.)
} elseif ($null -ne $dp.value) {
$vtype = "$($dp.valueType)"
if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable] -or $dp.value -is [System.Collections.IDictionary]) -and ($dp.value.variant)) {
$_hasDate = $false; $_hasSD = $false
if ($dp.value -is [PSCustomObject]) { $_hasDate = [bool]$dp.value.PSObject.Properties['date']; $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] }
else { $_hasDate = $dp.value.Contains('date'); $_hasSD = $dp.value.Contains('startDate') }
$_variantStr = "$($dp.value.variant)"
$_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*')
if ($_isSBD) {
$_d = $null
if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" }
elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" }
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>"
if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" }
X "$indent`t`t</dcscor:value>"
} else {
$_sd = $null; $_ed = $null
if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } }
else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } }
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>"
if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" }
X "$indent`t`t</dcscor:value>"
}
} elseif ($vtype -match '^[a-zA-Z]+:') {
$vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" }
X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>"
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>"
} elseif ($vtype -match '^date' -or "$($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>"
} elseif ($vtype -match '^decimal') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} elseif ($vtype -match '^string') {
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(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) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
X "$indent`t</dcscor:item>"
}
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
X "$indent</dcsset:dataParameters>"
}
function Emit-DLParameter {
param($p, $parsed, [string]$indent)
X "$indent<Parameter>"
$ci = "$indent`t"
X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>"
# Title: явный override (shorthand [..] / объект title/presentation) или авто из имени.
$title = $null
if ($parsed.title) { $title = $parsed.title }
elseif ($p -isnot [string] -and (Has-DLProp $p 'title') -and $p.title) { $title = $p.title }
elseif ($p -isnot [string] -and (Has-DLProp $p 'presentation') -and $p.presentation) { $title = $p.presentation }
if ($null -eq $title -or ($title -is [string] -and $title -eq '')) { $title = Title-FromName -name $parsed.name }
Emit-DLMLText -tag "dcssch:title" -text $title -indent $ci
# valueType
if ($parsed.type) { Emit-DLValueType -typeStr $parsed.type -indent $ci }
# value (дефолт nil; при valueListAllowed пустое — опускаем)
$vla = [bool]$parsed.valueListAllowed
$valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string])
if ($valIsArray) {
foreach ($v in @($parsed.value)) { Emit-DLValue -type $parsed.type -val $v -indent $ci -valueListAllowed $false }
} elseif ($parsed.valueExplicit -and ($null -ne $parsed.value) -and ("$($parsed.value)" -eq '') -and (("$($parsed.type)" -eq '') -or ("$($parsed.type)" -match '^string'))) {
# Явный пустой СТРОКОВЫЙ параметр (value:"" от декомпилятора) → типизированный пустой
# <dcssch:value xsi:type="xs:string"/>, НЕ nil. Решается ФОРМОЙ value (""→typed-empty,
# null/отсутствие→nil), независимо от valueListAllowed; декомпилятор различает ""/null
# (Convert-TypedValue пустого xs:string → "", nil → value опущен/null). Корпус: 26 xs:string.
X "$ci<dcssch:value xsi:type=`"xs:string`"/>"
} elseif ($vla -and (Test-DLEmptyValue $parsed.value) -and $parsed.valueExplicit) {
# valueListAllowed + явный пустой (value:null от декомпилятора) → платформа здесь пишет nil
X "$ci<dcssch:value xsi:nil=`"true`"/>"
} else {
Emit-DLValue -type $parsed.type -val $parsed.value -indent $ci -valueListAllowed $vla
}
# useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false.
$ur = $true
if ($p -isnot [string] -and (Has-DLProp $p 'useRestriction')) { $ur = [bool]$p.useRestriction }
X "$ci<dcssch:useRestriction>$(if ($ur) { 'true' } else { 'false' })</dcssch:useRestriction>"
# expression
$expr = $null
if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" }
if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" }
# availableValues
if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) {
foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci }
}
# valueListAllowed
if ($vla) { X "$ci<dcssch:valueListAllowed>true</dcssch:valueListAllowed>" }
# availableAsField=false (hidden или явный)
$aaf = $null
if ($parsed.hidden -eq $true) { $aaf = $false }
if ($p -isnot [string] -and (Has-DLProp $p 'availableAsField')) { $aaf = [bool]$p.availableAsField }
if ($aaf -eq $false) { X "$ci<dcssch:availableAsField>false</dcssch:availableAsField>" }
# inputParameters
if ($p -isnot [string] -and (Has-DLProp $p 'inputParameters') -and $p.inputParameters) { Emit-DLInputParameters -ip $p.inputParameters -indent $ci }
# denyIncompleteValues
if ($p -isnot [string] -and (Has-DLProp $p 'denyIncompleteValues') -and $p.denyIncompleteValues -eq $true) { X "$ci<dcssch:denyIncompleteValues>true</dcssch:denyIncompleteValues>" }
# use
$useVal = $null
if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" }
if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" }
X "$indent</Parameter>"
}
function Emit-DLParameters {
param($params, [string]$indent)
if (-not $params) { return }
foreach ($p in @($params)) {
if ($p -is [string]) {
$parsed = Parse-DLParamShorthand $p
} else {
$resolvedType = ""
if ((Has-DLProp $p 'type') -and $p.type) {
if ($p.type -is [array] -or ($p.type -is [System.Collections.IList] -and $p.type -isnot [string])) {
$resolvedType = (@($p.type | ForEach-Object { Resolve-TypeStr "$_" })) -join ' | '
} else { $resolvedType = Resolve-TypeStr "$($p.type)" }
} elseif ((Has-DLProp $p 'valueType') -and $p.valueType) {
$resolvedType = Resolve-TypeStr "$($p.valueType)"
}
$parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); valueExplicit = (Has-DLProp $p 'value'); title = $null }
if ((Has-DLProp $p 'valueListAllowed') -and $p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true }
if ((Has-DLProp $p 'hidden') -and $p.hidden -eq $true) { $parsed.hidden = $true }
}
Emit-DLParameter -p $p -parsed $parsed -indent $indent
}
}
function Emit-Attributes {
param($attrs, [string]$indent, $conditionalAppearance = $null)
$hasCA = $conditionalAppearance -and @($conditionalAppearance).Count -gt 0
# Платформа ВСЕГДА эмитит <Attributes> (100% корпуса; 162 формы — пустой <Attributes/>).
if ((-not $attrs -or $attrs.Count -eq 0) -and -not $hasCA) { X "$indent<Attributes/>"; return }
if (-not $attrs -or $attrs.Count -eq 0) {
# Нет реквизитов, но есть условное оформление (последний child <Attributes>)
X "$indent<Attributes>"
Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance'
X "$indent</Attributes>"
return
}
X "$indent<Attributes>"
$seenAttrs = @{}
foreach ($attr in $attrs) {
$attrId = New-Id
$attrName = "$($attr.name)"
Assert-UniqueName -name $attrName -seen $seenAttrs -kind 'attribute'
X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
$inner = "$indent`t`t"
# Title атрибута (зеркало Emit-Title): нет ключа → авто-вывод из имени (кроме main);
# title "" → подавить; непустой → эмитить как есть.
$hasTitleKey = $null -ne $attr.PSObject.Properties['title']
if ($hasTitleKey) {
if ($attr.title) { Emit-MLText -tag "Title" -text $attr.title -indent $inner }
} elseif ($attr.main -ne $true) {
Emit-MLText -tag "Title" -text (Title-FromName -name $attrName) -indent $inner
}
# Type
if ($attr.type) {
Emit-Type -typeStr "$($attr.type)" -indent $inner
} else {
X "$inner<Type/>"
}
# valueType: уточнение типа значений ValueList → <Settings xsi:type="v8:TypeDescription">
# (та же грамматика типа, что и Type, включая составной "A | B"). Forgiving-синонимы.
# Три состояния: нет ключа → нет Settings; "" → пустой <Settings…/>; тип → с типом.
$vtSpec = $null; $hasVt = $false
foreach ($k in @('valueType','typeDescription','описаниеТипов','типЗначений')) {
if ($attr.PSObject.Properties[$k]) { $vtSpec = $attr.$k; $hasVt = $true; break }
}
if ($hasVt) {
Emit-Type -typeStr "$vtSpec" -indent $inner -tag "Settings" -tagAttrs ' xsi:type="v8:TypeDescription"'
}
# Planner design-time <Settings xsi:type="pl:Planner"> (встроенный конфиг планировщика).
# Идёт сразу после <Type> (как valueType/DynamicList Settings — взаимоисключающи).
if ($attr.PSObject.Properties['planner'] -and $null -ne $attr.planner) {
Emit-PlannerSettings -pl $attr.planner -ind $inner
}
# Chart/GanttChart design-time <Settings xsi:type="d4p1:Chart"/"d4p1:GanttChart">.
# Тип Settings выводится из типа реквизита (d5p1:GanttChart → d4p1:GanttChart).
if ($attr.PSObject.Properties['chart'] -and $null -ne $attr.chart) {
$ctype = if ("$($attr.type)" -match 'GanttChart') { 'd4p1:GanttChart' } else { 'd4p1:Chart' }
Emit-ChartSettings -chart $attr.chart -ind $inner -ctype $ctype
}
if ($attr.main -eq $true) {
X "$inner<MainAttribute>true</MainAttribute>"
}
# Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute)
if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner }
if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner }
$mainSaved = $false
if ($attr.main -eq $true -and $attr.type) {
$mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.'
}
# Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто.
$emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved }
if ($emitSaved) {
X "$inner<SavedData>true</SavedData>"
}
# Save: сохранение значения реквизита в пользовательских настройках. true → <Field>имя</Field>;
# строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть).
# Нет ключа или false → не эмитим.
if ($null -ne $attr.PSObject.Properties['save'] -and $null -ne $attr.save) {
$saveFields = New-Object System.Collections.ArrayList
if ($attr.save -is [bool]) {
if ($attr.save) { [void]$saveFields.Add($attrName) }
} else {
foreach ($e in @($attr.save)) {
$fld = "$e"
if ([string]::IsNullOrEmpty($fld)) { continue }
if ($fld -ne $attrName -and $fld -notmatch '\.' -and $fld -notmatch '^\d+/\d+') { $fld = "$attrName.$fld" }
if (-not $saveFields.Contains($fld)) { [void]$saveFields.Add($fld) }
}
}
if ($saveFields.Count -gt 0) {
X "$inner<Save>"
foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" }
X "$inner</Save>"
}
}
# Проверка заполнения реквизита → <FillCheck> (реальный тег; <FillChecking> в схеме нет).
# bool true → ShowError (единственное значение в корпусе); строка → verbatim. Синоним fillChecking.
$fcRaw = if ($null -ne $attr.PSObject.Properties['fillCheck']) { $attr.fillCheck } elseif ($null -ne $attr.PSObject.Properties['fillChecking']) { $attr.fillChecking } else { $null }
if ($fcRaw) {
$fcv = if ($fcRaw -is [bool]) { 'ShowError' } else { "$fcRaw" }
X "$inner<FillCheck>$fcv</FillCheck>"
}
# UseAlways: поля, всегда читаемые (дин-список/таблица). Две формы DSL сливаются:
# attr.useAlways[] (короткие имена) + columns с useAlways:true → <Field>ИмяРеквизита.Поле</Field>.
$uaFields = New-Object System.Collections.ArrayList
if ($attr.useAlways) {
foreach ($e in @($attr.useAlways)) {
$fld = "$e"
# Префикс "ИмяРеквизита." добавляем к коротким именам. Поля дин-списка с маркером "~"
# (query-поля, ~13% корпуса) — префикс ставится ПОСЛЕ "~": ~Остановлен → ~Список.Остановлен.
# Полная форма (~Список.Остановлен / Список.Остановлен) — verbatim (forgiving ввод).
if ($fld.StartsWith('~')) {
$bare = $fld.Substring(1)
if ($bare -notmatch "^$([regex]::Escape($attrName))\.") { $bare = "$attrName.$bare" }
$fld = "~$bare"
} elseif ($fld -notmatch "^$([regex]::Escape($attrName))\." -and $fld -notmatch '^\d+/\d+') {
# UUID-ссылка (1/0:GUID) — НЕ префиксуем (платформа хранит её без "имя.")
$fld = "$attrName.$fld"
}
if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) }
}
}
if ($attr.columns) {
foreach ($col in $attr.columns) {
if ($col.useAlways -eq $true) {
$fld = "$attrName.$($col.name)"
if (-not $uaFields.Contains($fld)) { [void]$uaFields.Add($fld) }
}
}
}
if ($uaFields.Count -gt 0) {
X "$inner<UseAlways>"
foreach ($f in $uaFields) { X "$inner`t<Field>$f</Field>" }
X "$inner</UseAlways>"
}
Emit-FunctionalOptions -fo $attr.functionalOptions -indent $inner
# Columns: прямые <Column> (ValueTable/Tree) + <AdditionalColumns table="X"> (доп. колонки
# табличных частей объекта). Порядок схемы: прямые сначала, затем AdditionalColumns-группы.
# Для дин-списка (есть settings) прямые колонки НЕ эмитим (служат лишь для UseAlways).
$hasDirectCols = $attr.columns -and $attr.columns.Count -gt 0 -and -not $attr.settings
$hasAddCols = $attr.additionalColumns -and @($attr.additionalColumns).Count -gt 0
if ($hasDirectCols -or $hasAddCols) {
X "$inner<Columns>"
if ($hasDirectCols) {
$seenCols = @{} # колонки уникальны в пределах своего реквизита
foreach ($col in $attr.columns) {
Assert-UniqueName -name "$($col.name)" -seen $seenCols -kind "column of '$attrName'"
Emit-AttrColumn -col $col -indent "$inner`t"
}
}
if ($hasAddCols) {
foreach ($ac in @($attr.additionalColumns)) {
$acCols = @($ac.columns)
if ($acCols.Count -eq 0) {
# Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа)
X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>"
continue
}
X "$inner`t<AdditionalColumns table=`"$($ac.table)`">"
$seenAcCols = @{} # уникальность в пределах группы AdditionalColumns
foreach ($col in $acCols) {
Assert-UniqueName -name "$($col.name)" -seen $seenAcCols -kind "column of '$attrName'"
Emit-AttrColumn -col $col -indent "$inner`t`t"
}
X "$inner`t</AdditionalColumns>"
}
}
X "$inner</Columns>"
}
# Settings (динамический список)
if ($attr.settings) {
$st = $attr.settings
X "$inner<Settings xsi:type=`"DynamicList`">"
$si = "$inner`t"
# Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings
# AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение).
if ($null -ne $st.autoFillAvailableFields) { X "$si<AutoFillAvailableFields>$(if ($st.autoFillAvailableFields){'true'}else{'false'})</AutoFillAvailableFields>" }
$hasQuery = $st.query -and "$($st.query)".Trim()
# Явный ключ manualQuery (в т.ч. false) ПОБЕЖДАЕТ эвристику hasQuery (платформа изредка
# хранит QueryText при ManualQuery=false — декомпилятор фиксирует это отклонение).
$hasMQKey = ($st.PSObject.Properties['manualQuery']) -and ($null -ne $st.manualQuery)
$mq = if ($hasMQKey) { if ($st.manualQuery) { "true" } else { "false" } } elseif ($hasQuery) { "true" } else { "false" }
X "$si<ManualQuery>$mq</ManualQuery>"
# DynamicDataRead: дефолт true; false только при явном отключении
$ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" }
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
if ($hasQuery) {
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>"
}
# Явные поля набора (редко): override title/dataPath
if ($st.fields) {
foreach ($fld in $st.fields) {
# Тип поля набора: DataSetFieldField (дефолт) vs DataSetFieldNestedDataSet
# (поле-вложенный набор = реквизит табличной части; маркер nested).
# folder = папка-группировка полей (DataSetFieldFolder, без <field>); nested = вложенный набор.
$isFolder = [bool](Get-Prop $fld 'folder')
$ftype = if ($fld.nested) { "DataSetFieldNestedDataSet" } elseif ($isFolder) { "DataSetFieldFolder" } else { "DataSetFieldField" }
X "$si<Field xsi:type=`"dcssch:$ftype`">"
# dataPath: явный (включая пустой "" → self-closing <dcssch:dataPath/>) побеждает; иначе fallback на field.
if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" }
elseif ($isFolder) { $dp = "" }
else { $dp = "$($fld.field)" }
if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" }
if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" }
if ($fld.title) {
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
Emit-MLItems -val $fld.title -indent "$si`t`t"
X "$si`t</dcssch:title>"
}
# Ограничения использования поля — после title, перед presentationExpression (порядок исходника)
Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t"
Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t"
# presentationExpression поля — перед valueType (порядок исходника)
if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-Xml "$($fld.presentationExpression)")</dcssch:presentationExpression>" }
# valueType поля набора (тип значения; вычисляемые/кастомные поля)
if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" }
# appearance поля (формат/оформление) — после valueType (порядок исходника)
if ($fld.appearance) {
X "$si`t<dcssch:appearance>"
foreach ($prop in $fld.appearance.PSObject.Properties) { Emit-AppearanceValue -key $prop.Name -val $prop.Value -indent "$si`t`t" }
X "$si`t</dcssch:appearance>"
}
# inputParameters поля (связь по параметрам выбора) — в конце
if ($fld.inputParameters) { Emit-DLInputParameters -ip $fld.inputParameters -indent "$si`t" }
X "$si</Field>"
}
}
# Вычисляемые поля DataSet (<CalculatedField>) — после Field*, до Parameter*.
Emit-CalcFields -calcFields $st.calculatedFields -indent $si
# Schema-параметры дин-списка (DataCompositionSchemaParameter) — после Field*, до MainTable.
Emit-DLParameters -params $st.parameters -indent $si
# Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey)
# + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений.
if ($st.keyType) { X "$si<KeyType>$(Esc-Xml "$($st.keyType)")</KeyType>" }
if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-Xml "$kf")</KeyField>" } }
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
# GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false).
if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" }
# AutoSaveUserSettings — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении).
if ($null -ne $st.autoSaveUserSettings) { X "$si<AutoSaveUserSettings>$(if ($st.autoSaveUserSettings){'true'}else{'false'})</AutoSaveUserSettings>" }
# ListSettings: filter/order/conditionalAppearance (skd-грамматика) + каноничные блок-GUID.
# Нет items → контейнеры всё равно эмитятся (blockMeta) = каноничный пустой скелет платформы.
$lsi = "$si`t"
$lsOpenLen = $script:xml.Length
X "$si<ListSettings>"
$lsAfterOpenLen = $script:xml.Length # для self-closing, если внутри ничего не эмитнётся
if ($st.PSObject.Properties['listSettings'] -and $null -ne $st.listSettings) {
# Частичная/минимальная форма скелета — эмитим ТОЛЬКО указанные части с их блок-метой.
# meta: 'v'=viewMode, 'u'=userSettingID (контейнеры); itemsViewMode/itemsUserSettingID → present.
foreach ($prop in $st.listSettings.PSObject.Properties) {
$tag = $prop.Name; $pv = $prop.Value
# Значение дескриптора: строка-код "vu" ИЛИ объект { meta:"vu", presentation:<текст/ML> }
# (контейнер несёт собственный userSettingPresentation — кастомную подпись настройки).
if (($pv -is [PSCustomObject]) -or ($pv -is [System.Collections.IDictionary])) {
$meta = "$(Get-Prop $pv 'meta')"; $bpres = Get-Prop $pv 'presentation'
} else { $meta = "$pv"; $bpres = $null }
$bvm = if ($meta -match 'v') { 'Normal' } else { $null }
switch ($tag) {
'filter' { $bus = if ($meta -match 'u') { $script:CANON_FILTER_ID } else { $null }; Emit-Filter -items $st.filter -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres }
'order' { $bus = if ($meta -match 'u') { $script:CANON_ORDER_ID } else { $null }; Emit-Order -items $st.order -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres }
'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus -blockUserSettingPresentation $bpres }
'itemsViewMode' { X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>" }
'itemsUserSettingID' { X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>" }
'itemsUserSettingPresentation' { Emit-USPresentation -val $pv -tag "dcsset:itemsUserSettingPresentation" -indent $lsi }
'dataParameters' { Emit-DataParameters -items $st.dataParameters -indent $lsi }
'structure' { Emit-ListGrouping (Get-ListGroupingValue $st) $lsi }
}
}
} else {
# Полный каноничный скелет (умолчание, ~93% форм) — без изменений.
Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID
# dataParameters — после filter, до order (XSD-порядок ListSettings)
if ($st.PSObject.Properties['dataParameters']) { Emit-DataParameters -items $st.dataParameters -indent $lsi }
Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID
Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID
# Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode
Emit-ListGrouping (Get-ListGroupingValue $st) $lsi
X "$lsi<dcsset:itemsViewMode>Normal</dcsset:itemsViewMode>"
X "$lsi<dcsset:itemsUserSettingID>$($script:CANON_ITEMS_ID)</dcsset:itemsUserSettingID>"
}
if ($script:xml.Length -eq $lsAfterOpenLen) {
# Пустой дескриптор listSettings:{} (оригинал = <ListSettings/>) → зеркалим self-closing.
$script:xml.Length = $lsOpenLen
X "$si<ListSettings/>"
} else {
X "$si</ListSettings>"
}
X "$inner</Settings>"
}
X "$indent`t</Attribute>"
}
# Условное оформление формы — последний child <Attributes> (та же DCS-грамматика, что settings CA)
Emit-ConditionalAppearance -items $conditionalAppearance -indent "$indent`t" -wrapTag 'ConditionalAppearance'
X "$indent</Attributes>"
}
# --- 9. Parameter emitter ---
function Emit-Parameters {
param($params, [string]$indent)
if (-not $params -or $params.Count -eq 0) { return }
X "$indent<Parameters>"
$seenParams = @{}
foreach ($param in $params) {
Assert-UniqueName -name "$($param.name)" -seen $seenParams -kind 'parameter'
X "$indent`t<Parameter name=`"$($param.name)`">"
$inner = "$indent`t`t"
Emit-Type -typeStr "$($param.type)" -indent $inner
if ($param.key -eq $true) {
X "$inner<KeyParameter>true</KeyParameter>"
}
X "$indent`t</Parameter>"
}
X "$indent</Parameters>"
}
# --- 10. Command emitter ---
function Emit-Commands {
param($cmds, [string]$indent)
if (-not $cmds -or $cmds.Count -eq 0) { return }
X "$indent<Commands>"
$seenCmds = @{}
foreach ($cmd in $cmds) {
$cmdId = New-Id
Assert-UniqueName -name "$($cmd.name)" -seen $seenCmds -kind 'command'
X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
$inner = "$indent`t`t"
# Заголовок команды (зеркало Emit-Title): ключ есть+непустой → эмитим; ключ есть+"" → суппресс
# (в оригинале <Title> нет — не додумывать); ключ отсутствует → авто-вывод из имени (помощь модели).
if ($null -ne $cmd.PSObject.Properties['title']) {
if ($cmd.title) { Emit-MLText -tag "Title" -text $cmd.title -indent $inner }
} else {
$cmdTitle = Title-FromName -name "$($cmd.name)"
if ($cmdTitle) { Emit-MLText -tag "Title" -text $cmdTitle -indent $inner }
}
if ($cmd.tooltip) {
Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner
}
# Доступность команды по ролям (после ToolTip, до Action)
if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner }
if ($cmd.action) {
X "$inner<Action>$($cmd.action)</Action>"
}
if ($cmd.modifiesSavedData -eq $true) { X "$inner<ModifiesSavedData>true</ModifiesSavedData>" }
Emit-FunctionalOptions -fo $cmd.functionalOptions -indent $inner
if ($cmd.currentRowUse) {
X "$inner<CurrentRowUse>$($cmd.currentRowUse)</CurrentRowUse>"
}
# Используемая таблица — имя элемента-таблицы (xsi:type обязателен).
# Forgiving-ключи: table / associatedTableElementId (XML-тег) / ИспользуемаяТаблица (рус., регистр-незав.)
$cmdTable = $cmd.table
if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId }
if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица }
if ($cmdTable) {
X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>"
}
if ($cmd.shortcut) {
X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>"
}
Emit-CommandPicture -pic $cmd.picture -elemLt $cmd.loadTransparent -indent $inner
if ($cmd.representation) {
X "$inner<Representation>$($cmd.representation)</Representation>"
}
X "$indent`t</Command>"
}
X "$indent</Commands>"
}
# Резолв ключа-группы древовидной формы → CommandGroup (зависит от панели). Дружелюбные
# алиасы стандартных form-групп; любой иной ключ (CommandGroup.X / GUID / "") — verbatim.
function Resolve-CommandGroupKey {
param([string]$key, [string]$panelTag)
$k = ($key -replace '\s','').ToLower()
if ($panelTag -eq 'NavigationPanel') {
switch ($k) {
'important' { return 'FormNavigationPanelImportant' }
'важное' { return 'FormNavigationPanelImportant' }
'goto' { return 'FormNavigationPanelGoTo' }
'перейти' { return 'FormNavigationPanelGoTo' }
'seealso' { return 'FormNavigationPanelSeeAlso' }
'смтакже' { return 'FormNavigationPanelSeeAlso' }
}
} else {
switch ($k) {
'important' { return 'FormCommandBarImportant' }
'важное' { return 'FormCommandBarImportant' }
'createbasedon' { return 'FormCommandBarCreateBasedOn' }
'создатьнаосновании' { return 'FormCommandBarCreateBasedOn' }
}
}
return $key # verbatim
}
# Командный интерфейс формы (<CommandInterface>): панели CommandBar + NavigationPanel.
# Значение панели: МАССИВ (плоская форма; элемент может нести group) ИЛИ ОБЪЕКТ
# (древовидная форма: {группа: [команды]}, group берётся из ключа, элементы его не дублируют).
# Элемент: строка (голый command, Type=Auto) или объект. Порядок тегов:
# Command, Type(деф. Auto), Attribute, CommandGroup, Index, DefaultVisible, Visible(xr-flag).
function Emit-CommandInterface {
param($ci, [string]$indent)
if (-not $ci) { return }
$inner = "$indent`t"
$panels = @(
@{ Tag='CommandBar'; Syns=@('commandBar','команднаяПанель','КоманднаяПанель') },
@{ Tag='NavigationPanel'; Syns=@('navigationPanel','панельНавигации','ПанельНавигации') }
)
$present = @()
foreach ($p in $panels) {
$items = $null
foreach ($syn in $p.Syns) { if ($null -ne $ci.PSObject.Properties[$syn]) { $items = $ci.($syn); break } }
if ($null -ne $items) { $present += ,@{ Tag=$p.Tag; Items=$items } }
}
if ($present.Count -eq 0) { return }
X "$indent<CommandInterface>"
foreach ($p in $present) {
X "$inner<$($p.Tag)>"
# Нормализация: плоский список пар (элемент, group-из-дерева). Объект → дерево.
$flat = New-Object System.Collections.ArrayList
if ($p.Items -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in $p.Items.PSObject.Properties) {
$grpFromTree = Resolve-CommandGroupKey -key $prop.Name -panelTag $p.Tag
foreach ($it in @($prop.Value)) { [void]$flat.Add(@{ item=$it; treeGroup=$grpFromTree }) }
}
} else {
foreach ($it in @($p.Items)) { [void]$flat.Add(@{ item=$it; treeGroup=$null }) }
}
foreach ($fi in $flat) {
$item = $fi.item; $treeGroup = $fi.treeGroup
if ($item -is [string]) {
$cmd = $item; $type = 'Auto'; $attr = $null; $grp = $null; $idx = $null; $dv = $null; $vis = $null
} else {
$cmd = Get-ElProp $item @('command','команда')
$type = Get-ElProp $item @('type','тип'); if (-not $type) { $type = 'Auto' }
$attr = Get-ElProp $item @('attribute','реквизит')
$grp = Get-ElProp $item @('group','группа','группаКоманд')
$idx = Get-ElProp $item @('index','индекс')
$dv = Get-ElProp $item @('defaultVisible','видимость','видимостьПоУмолчанию')
$vis = Get-ElProp $item @('visible','видимостьПоРолям','настройкаВидимости')
}
# group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк
if ($treeGroup) { $grp = $treeGroup }
X "$inner`t<Item>"
X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>"
X "$inner`t`t<Type>$type</Type>"
if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" }
if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" }
if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" }
if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" }
if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" }
X "$inner`t</Item>"
}
X "$inner</$($p.Tag)>"
}
X "$indent</CommandInterface>"
}
# --- 11. Properties emitter ---
function Emit-Properties {
param($props, [string]$indent)
if (-not $props) { return }
# camelCase -> PascalCase mapping for known properties
$propMap = @{
"autoTitle" = "AutoTitle"
"windowOpeningMode" = "WindowOpeningMode"
"commandBarLocation" = "CommandBarLocation"
"saveDataInSettings" = "SaveDataInSettings"
"autoSaveDataInSettings" = "AutoSaveDataInSettings"
"autoTime" = "AutoTime"
"usePostingMode" = "UsePostingMode"
"repostOnWrite" = "RepostOnWrite"
"autoURL" = "AutoURL"
"autoFillCheck" = "AutoFillCheck"
"customizable" = "Customizable"
"enterKeyBehavior" = "EnterKeyBehavior"
"verticalScroll" = "VerticalScroll"
"scalingMode" = "ScalingMode"
"useForFoldersAndItems" = "UseForFoldersAndItems"
"reportResult" = "ReportResult"
"detailsData" = "DetailsData"
"reportFormType" = "ReportFormType"
"autoShowState" = "AutoShowState"
"width" = "Width"
"height" = "Height"
"group" = "Group"
}
foreach ($p in $props.PSObject.Properties) {
$xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else {
# Auto PascalCase: first letter uppercase
$p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1)
}
# Convert boolean to lowercase string (PS renders as True/False)
$val = $p.Value
# Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать)
if ($val -is [string] -and $val -eq '') { continue }
if ($val -is [bool]) {
$val = if ($val) { "true" } else { "false" }
}
X "$indent<$xmlName>$val</$xmlName>"
}
}
# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
# Companion-панели как СВОЙСТВА элемента (значение объект/массив): любой знакомый
# синоним → каноника commandBar / contextMenu. Разводим с одноимёнными ТИПАМИ-элементами
# по типу значения: строка = элемент-тип (имя), объект/массив = панель-свойство.
function Normalize-PanelSynonyms {
param($el)
if ($null -eq $el) { return }
$panelSyns = @{
'commandBar' = @('commandBar','autoCommandBar','AutoCommandBar','autoCmdBar','cmdBar','КоманднаяПанель')
'contextMenu' = @('contextMenu','ContextMenu','КонтекстноеМеню')
}
foreach ($canon in $panelSyns.Keys) {
foreach ($syn in $panelSyns[$canon]) {
$p = $el.PSObject.Properties[$syn]
if ($null -ne $p -and ($p.Value -is [array] -or $p.Value -is [System.Management.Automation.PSCustomObject])) {
if ($syn -ne $canon -and $null -eq $el.PSObject.Properties[$canon]) {
$v = $p.Value
$el.PSObject.Properties.Remove($syn) | Out-Null
$el | Add-Member -NotePropertyName $canon -NotePropertyValue $v -Force
}
break
}
}
}
}
function Normalize-ElementSynonyms {
param($el)
if ($null -eq $el) { return }
Normalize-PanelSynonyms $el
# Тип-синонимы (commandBar/autoCommandBar → элемент-тип) применяем ТОЛЬКО к строковому
# значению (имя элемента); объект/массив уже отнесён к панель-свойству выше.
$typeSyn = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
foreach ($pair in $typeSyn.GetEnumerator()) {
$src = $el.PSObject.Properties[$pair.Key]
if ($null -ne $src -and ($src.Value -is [string]) -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
if ($el.PSObject.Properties["extTooltip"] -and $null -eq $el.PSObject.Properties["extendedTooltip"]) {
$val = $el.extTooltip
$el.PSObject.Properties.Remove("extTooltip") | Out-Null
$el | Add-Member -NotePropertyName "extendedTooltip" -NotePropertyValue $val -Force
}
# Рекурсия в детей панелей (commandBar/contextMenu) — нормализуем кнопки/группы внутри
foreach ($pk in @('commandBar','contextMenu')) {
$pp = $el.PSObject.Properties[$pk]
if ($null -ne $pp) {
$kids = if ($pp.Value -is [array]) { $pp.Value } elseif ($null -ne $pp.Value) { $pp.Value.children } else { $null }
if ($kids) { foreach ($child in $kids) { Normalize-ElementSynonyms $child } }
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { Normalize-ElementSynonyms $child }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { Normalize-ElementSynonyms $child }
}
}
function HasCmdBarRecursive {
param($el)
if ($null -eq $el) { return $false }
if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true }
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } }
}
return $false
}
function ApplyDynamicListTableHeuristic {
param($el, [string]$listName, [bool]$hasMainTable)
if ($null -eq $el) { return }
if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) {
# Маркер дин-список-таблицы → Emit-Table эмитит блок свойств (Group A defaults)
$el | Add-Member -NotePropertyName "_dynList" -NotePropertyValue $true -Force
if ($null -eq $el.PSObject.Properties["tableAutofill"]) {
$el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force
}
if ($null -eq $el.PSObject.Properties["commandBarLocation"]) {
$el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force
}
# RowPictureDataPath: умный дефолт <Список>.DefaultPicture, если ключ ОТСУТСТВУЕТ.
# Декомпилятор опускает ключ при rpdp == smart-default (ждёт реинъекции); реальное отсутствие
# фиксирует ""-маркером (НЕ перезатирается). Гейт hasMainTable снят: дин-список без mainTable
# (напр. query-based) тоже несёт RowPictureDataPath.
if ($null -eq $el.PSObject.Properties["rowPictureDataPath"]) {
$el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable }
}
}
function Test-IsObjectLikeType {
param([string]$type)
if ([string]::IsNullOrEmpty($type)) { return $false }
if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true }
$objectSuffixes = @(
"CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject",
"ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject",
"TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject",
"ChartOfCalculationTypesObject", "ExchangePlanObject"
)
$recordSetPrefixes = @(
"InformationRegisterRecordSet", "AccumulationRegisterRecordSet",
"AccountingRegisterRecordSet", "CalculationRegisterRecordSet",
"InformationRegisterRecordManager"
)
foreach ($suffix in $objectSuffixes) {
if ($type -like "$suffix.*") { return $true }
}
foreach ($prefix in $recordSetPrefixes) {
if ($type -like "$prefix.*") { return $true }
}
return $false
}
# 11b.1: Normalize synonyms recursively
if ($def.elements) {
foreach ($el in $def.elements) { Normalize-ElementSynonyms $el }
}
# 11b.2: Extract autoCmdBar element from def.elements
$script:mainAcbDef = $null
if ($def.elements) {
$autoBars = @()
$rest = @()
foreach ($el in $def.elements) {
if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) {
$autoBars += $el
} else {
$rest += $el
}
}
if ($autoBars.Count -gt 1) {
Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed."
exit 1
}
if ($autoBars.Count -eq 1) {
$script:mainAcbDef = $autoBars[0]
# Replace def.elements with the filtered list
$def.PSObject.Properties.Remove("elements") | Out-Null
$def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force
}
}
# 11b.3: Infer main attribute (only if no attribute has main:true)
if ($def.attributes) {
$hasExplicitMain = $false
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $hasExplicitMain = $true; break }
}
if (-not $hasExplicitMain) {
$candidates = @()
foreach ($attr in $def.attributes) {
# Skip if user explicitly opted out via main:false
if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue }
if (Test-IsObjectLikeType "$($attr.type)") {
$candidates += $attr
}
}
if ($candidates.Count -eq 1) {
$candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force
Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))"
} elseif ($candidates.Count -gt 1) {
$names = ($candidates | ForEach-Object { $_.name }) -join ", "
Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly"
}
}
}
# 11b.4: DynamicList → table heuristic (для ВСЕХ DynamicList-реквизитов, не только main)
if ($def.attributes -and $def.elements) {
foreach ($attr in $def.attributes) {
if ("$($attr.type)" -ne "DynamicList") { continue }
$mt = $null
if ($attr.PSObject.Properties["settings"] -and $null -ne $attr.settings) {
if ($attr.settings -is [hashtable]) {
if ($attr.settings.ContainsKey("mainTable")) { $mt = $attr.settings["mainTable"] }
} elseif ($attr.settings.PSObject.Properties["mainTable"]) {
$mt = $attr.settings.mainTable
}
}
$hasMt = -not [string]::IsNullOrEmpty("$mt")
foreach ($el in $def.elements) {
ApplyDynamicListTableHeuristic $el $attr.name $hasMt
}
}
}
# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3
function Compute-MainAcbAutofill {
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) {
return [bool]$script:mainAcbDef.autofill
}
return $true
}
if ($def.elements) {
foreach ($el in $def.elements) {
if (HasCmdBarRecursive $el) { return $false }
}
}
return $true
}
# --- 12. Main compilation ---
# Title
if ($def.title) {
Emit-MLText -tag "Title" -text $def.title -indent "`t"
}
# Header
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
# Oops — Title was emitted before header. Need to fix the order.
# Actually, let me restructure: build the body into a separate buffer, then assemble
# Reset and rebuild properly
$script:xml = New-Object System.Text.StringBuilder 8192
$script:nextId = 1
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
# 12a. Title (from def.title or properties.title — must be multilingual XML)
$formTitle = $def.title
if (-not $formTitle -and $def.properties -and $def.properties.title) {
$formTitle = $def.properties.title
}
if ($formTitle) {
Emit-MLText -tag "Title" -text $formTitle -indent "`t"
}
# 12b. Properties (skip 'title' — handled above as multilingual)
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
$propsClone = New-Object PSObject
$hasAutoTitle = $false
if ($def.properties) {
foreach ($p in $def.properties.PSObject.Properties) {
if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true }
}
}
if ($formTitle -and -not $hasAutoTitle) {
$propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false
}
if ($def.properties) {
foreach ($p in $def.properties.PSObject.Properties) {
if ($p.Name -ne "title") {
$propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value
}
}
}
Emit-Properties -props $propsClone -indent "`t"
# 12c. CommandSet (excluded commands)
if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
X "`t<CommandSet>"
foreach ($cmd in $def.excludedCommands) {
X "`t`t<ExcludedCommand>$cmd</ExcludedCommand>"
}
X "`t</CommandSet>"
}
# 12c2. MobileDeviceCommandBarContent — форменный список имён командных панелей/кнопок
# (Presentation пустой, CheckState=0, тип xs:string — константы; варьируется только имя-Value).
# $null-проверка (не truthy): одноэлементный массив с пустой строкой @("") разворачивается
# в boolean-контексте в "" → falsy; 12 форм корпуса несут один пустой item (Value="").
if ($null -ne $def.mobileCommandBarContent -and @($def.mobileCommandBarContent).Count -gt 0) {
X "`t<MobileDeviceCommandBarContent>"
foreach ($nm in @($def.mobileCommandBarContent)) {
X "`t`t<xr:Item>"
X "`t`t`t<xr:Presentation/>"
X "`t`t`t<xr:CheckState>0</xr:CheckState>"
# пустое значение → самозакрывающийся тег (зеркало платформы)
if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" }
else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" }
X "`t`t</xr:Item>"
}
X "`t</MobileDeviceCommandBarContent>"
}
# 12d. AutoCommandBar (always present, id=-1)
$acbAutofill = Compute-MainAcbAutofill
$acbName = "ФормаКоманднаяПанель"
$acbHAlign = $null
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") {
$acbName = "$($script:mainAcbDef.autoCmdBar)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") {
$acbName = "$($script:mainAcbDef.name)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") {
$acbHAlign = "$($script:mainAcbDef.horizontalAlign)"
}
}
$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0)
$acbDIAttr = if ($script:mainAcbDef) { DI-Attr $script:mainAcbDef } else { "" } # DisplayImportance форменной панели
$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren)
if ($acbHasInner) {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"$acbDIAttr>"
if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" }
if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" }
if ($hasAcbChildren) {
X "`t`t<ChildItems>"
foreach ($child in $script:mainAcbDef.children) {
Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true
}
X "`t`t</ChildItems>"
}
X "`t</AutoCommandBar>"
} else {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"$acbDIAttr/>"
}
# 12e. Events
if ($def.events) {
foreach ($p in $def.events.PSObject.Properties) {
if ($script:knownFormEvents -notcontains $p.Name) {
Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')"
}
}
X "`t<Events>"
foreach ($p in $def.events.PSObject.Properties) {
X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>"
}
X "`t</Events>"
}
# 12f. ChildItems (elements)
if ($def.elements -and $def.elements.Count -gt 0) {
X "`t<ChildItems>"
foreach ($el in $def.elements) {
Emit-Element -el $el -indent "`t`t"
}
X "`t</ChildItems>"
}
# 12g. Attributes
Emit-Attributes -attrs $def.attributes -indent "`t" -conditionalAppearance $def.conditionalAppearance
# 12h. Parameters
Emit-Parameters -params $def.parameters -indent "`t"
# 12i. Commands
Emit-Commands -cmds $def.commands -indent "`t"
# 12i2. CommandInterface (командный интерфейс формы — последний дочерний Form)
Emit-CommandInterface -ci $def.commandInterface -indent "`t"
# 12j. Close
X '</Form>'
# --- 13. Write output ---
$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$outDir = [System.IO.Path]::GetDirectoryName($outPath)
if (-not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc)
# --- 13b. Auto-register form in parent object XML ---
# Infer parent from OutputPath: .../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml
$formXmlDir = [System.IO.Path]::GetDirectoryName($outPath)
$formNameDir = [System.IO.Path]::GetDirectoryName($formXmlDir)
$formsDir = [System.IO.Path]::GetDirectoryName($formNameDir)
$objectDir = [System.IO.Path]::GetDirectoryName($formsDir)
$typePluralDir = [System.IO.Path]::GetDirectoryName($objectDir)
$formName = [System.IO.Path]::GetFileName($formNameDir)
$objectName = [System.IO.Path]::GetFileName($objectDir)
$formsLeaf = [System.IO.Path]::GetFileName($formsDir)
if ($formsLeaf -eq 'Forms') {
$objectXmlPath = Join-Path $typePluralDir "$objectName.xml"
if (Test-Path $objectXmlPath) {
$objDoc = New-Object System.Xml.XmlDocument
$objDoc.PreserveWhitespace = $true
$objDoc.Load($objectXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $objDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if ($childObjects) {
$existing = $childObjects.SelectSingleNode("md:Form[text()='$formName']", $nsMgr)
if (-not $existing) {
$formElem = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $formName
$insertBefore = $childObjects.SelectSingleNode("md:Template", $nsMgr)
if (-not $insertBefore) { $insertBefore = $childObjects.SelectSingleNode("md:TabularSection", $nsMgr) }
if ($insertBefore) {
$childObjects.InsertBefore($formElem, $insertBefore) | Out-Null
$ws = $objDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($ws, $insertBefore) | Out-Null
} else {
$lastChild = $childObjects.LastChild
if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($objDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($objDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
$regEnc = New-Object System.Text.UTF8Encoding($true)
$regSettings = New-Object System.Xml.XmlWriterSettings
$regSettings.Encoding = $regEnc
$regSettings.Indent = $false
$regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create)
$regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings)
$objDoc.Save($regWriter)
$regWriter.Close()
$regStream.Close()
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
}
}
}
}
# --- 14. Summary ---
$elCount = $script:nextId - 1
Write-Host "[OK] Compiled: $OutputPath"
Write-Host " Elements+IDs: $elCount"
if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" }
if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" }
if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" }