Files
cc-1c-skills/.claude/skills/form-compile/scripts/form-compile.ps1
T
Nick Shirokov 52478a6c39 fix(form-compile): эмитить <ChoiceButton>true</ChoiceButton> при choiceButton:true + StartChoice
Компилятор выводил тег <ChoiceButton> только для значения false; при choiceButton:true
он не эмитился, и у нессылочного поля (например строкового с обработчиком НачалоВыбора)
кнопка выбора не отрисовывалась — документированный паттерн (SKILL.md: choiceButton:true
+ on:['StartChoice']) фактически не работал.

Теперь true эмитится, но узко: только когда у поля есть обработчик StartChoice — чтобы
не раздувать вывод по ссылочным полям (у них choiceButton=true стоит по умолчанию,
а кнопка платформенная). Порты ps1+py синхронны. Снапшот file-dialog обновлён,
31/31 кейс зелёные на обоих портах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:26:23 +03:00

3390 lines
128 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.23 — 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 }
# Apply ref defaults
if ($isRef -and $fieldDefaults -and $fieldDefaults.ref) {
if ($fieldDefaults.ref.choiceButton -eq $true) { $el["choiceButton"] = $true }
}
# 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) {
$edCols = @()
$edCols += [ordered]@{ input = "ВидСубконто"; path = "Объект.ExtDimensionTypes.ExtDimensionType" }
$edCols += [ordered]@{ check = "ТолькоОбороты"; path = "Объект.ExtDimensionTypes.TurnoversOnly" }
if ($meta.ExtDimensionAccountingFlags) {
foreach ($edFlag in $meta.ExtDimensionAccountingFlags) {
$edCols += [ordered]@{ check = $edFlag.Name; path = "Объект.ExtDimensionTypes.$($edFlag.Name)" }
}
}
$elements += [ordered]@{
table = "ВидыСубконто"
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
}
# --- 2. ID allocator ---
$script:nextId = 1
function New-Id {
$id = $script:nextId
$script:nextId++
return $id
}
# --- 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 {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
# --- 4. Multilang helper ---
function Emit-MLText {
param([string]$tag, [string]$text, [string]$indent)
X "$indent<$tag>"
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>ru</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
X "$indent`t</v8:item>"
X "$indent</$tag>"
}
# --- 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"
# 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 {
param($typeStr, [string]$indent)
if (-not $typeStr) {
X "$indent<Type/>"
return
}
$typeString = "$typeStr"
# Composite type: "Type1 | Type2" or "Type1 + Type2"
$parts = $typeString -split '\s*[|+]\s*'
X "$indent<Type>"
foreach ($part in $parts) {
$part = $part.Trim()
Emit-SingleType -typeStr $part -indent "$indent`t"
}
X "$indent</Type>"
}
function Emit-SingleType {
param([string]$typeStr, [string]$indent)
$typeStr = Resolve-TypeStr $typeStr
# boolean
if ($typeStr -eq "boolean") {
X "$indent<v8:Type>xs:boolean</v8:Type>"
return
}
# string or string(N)
if ($typeStr -match '^string(\((\d+)\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "0" }
X "$indent<v8:Type>xs:string</v8:Type>"
X "$indent<v8:StringQualifiers>"
X "$indent`t<v8:Length>$len</v8:Length>"
X "$indent`t<v8:AllowedLength>Variable</v8:AllowedLength>"
X "$indent</v8:StringQualifiers>"
return
}
# decimal(D,F) or decimal(D,F,nonneg)
if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') {
$digits = $Matches[1]
$fraction = $Matches[2]
$sign = if ($Matches[3]) { "Nonnegative" } else { "Any" }
X "$indent<v8:Type>xs:decimal</v8:Type>"
X "$indent<v8:NumberQualifiers>"
X "$indent`t<v8:Digits>$digits</v8:Digits>"
X "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>"
X "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>"
X "$indent</v8:NumberQualifiers>"
return
}
# date / dateTime / 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
}
}
# DynamicList
if ($typeStr -eq "DynamicList") {
X "$indent<v8:Type>cfg:DynamicList</v8:Type>"
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
}
# Fallback with validation
if ($script:knownInvalidTypes.ContainsKey($typeStr)) {
throw "Invalid form attribute type '$typeStr': $($script:knownInvalidTypes[$typeStr])"
}
if ($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")
function Emit-Events {
param($el, [string]$elementName, [string]$indent, [string]$typeKey)
if (-not $el.on) { return }
# Validate event names
if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) {
$allowed = $script:knownEvents[$typeKey]
foreach ($evt in $el.on) {
if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") {
Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')"
}
}
}
X "$indent<Events>"
foreach ($evt in $el.on) {
$evtName = "$evt"
$handler = if ($el.handlers -and $el.handlers.$evtName) {
"$($el.handlers.$evtName)"
} else {
Get-HandlerName -elementName $elementName -eventName $evtName
}
X "$indent`t<Event name=`"$evtName`">$handler</Event>"
}
X "$indent</Events>"
}
function Emit-Companion {
param([string]$tag, [string]$name, [string]$indent)
$id = New-Id
X "$indent<$tag name=`"$name`" id=`"$id`"/>"
}
function Emit-Element {
param($el, [string]$indent, [bool]$inCmdBar = $false)
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
# Maps any synonym to canonical short DSL key.
$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"
}
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -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
}
}
# Determine element type from key
$typeKey = $null
$xmlTag = $null
foreach ($key in @("columnGroup","group","input","check","radio","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
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;"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
# columnGroup-specific
"showInHeader"=1
# radio-specific
"radioButtonType"=1;"choiceList"=1;"columnsCount"=1
# naming & binding
"name"=1;"path"=1;"title"=1
# visibility & state
"visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1
# events
"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
# input-specific
"multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1
"spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1
"textEdit"=1
# label/hyperlink
"hyperlink"=1
# group-specific
"showTitle"=1;"united"=1;"collapsed"=1
# hierarchy
"children"=1;"columns"=1
# table-specific
"changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1
"commandBarLocation"=1;"searchStringLocation"=1
"choiceMode"=1;"initialTreeView"=1;"enableDrag"=1;"enableStartDrag"=1
"rowPictureDataPath"=1;"tableAutofill"=1
# pages-specific
"pagesRepresentation"=1
# button-specific
"type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1
# picture/decoration
"src"=1;"valuesPicture"=1;"loadTransparent"=1
# cmdBar-specific
"autofill"=1
}
foreach ($p in $el.PSObject.Properties) {
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
$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 }
"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 }
"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 }
}
}
function Emit-CommonFlags {
param($el, [string]$indent)
if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" }
if ($el.userVisible -eq $false) {
X "$indent<UserVisible>"
X "$indent`t<xr:Common>false</xr:Common>"
X "$indent</UserVisible>"
}
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>" }
}
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 {
param($el, [string]$name, [string]$indent, [switch]$auto)
$title = $el.title
if (-not $title -and $auto -and $name) {
$title = Title-FromName -name $name
}
if ($title) {
Emit-MLText -tag "Title" -text "$title" -indent $indent
}
}
function Emit-Group {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<UsualGroup name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
# Group orientation
$groupVal = "$($el.group)"
$orientation = switch ($groupVal) {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"alwaysHorizontal" { "AlwaysHorizontal" }
"alwaysVertical" { "AlwaysVertical" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
# Behavior
if ($groupVal -eq "collapsible") {
X "$inner<Group>Vertical</Group>"
X "$inner<Behavior>Collapsible</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>"
}
# ShowTitle
if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" }
# United
if ($el.united -eq $false) { X "$inner<United>false</United>" }
Emit-CommonFlags -el $el -indent $inner
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
# 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`">"
$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>" }
if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" }
if ($null -ne $el.showInHeader) {
$shVal = if ($el.showInHeader) { "true" } else { "false" }
X "$inner<ShowInHeader>$shVal</ShowInHeader>"
}
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
Emit-CommonFlags -el $el -indent $inner
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
# 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`">"
$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 ($el.multiLine -eq $true) { X "$inner<MultiLine>true</MultiLine>" }
if ($el.passwordMode -eq $true) { X "$inner<PasswordMode>true</PasswordMode>" }
if ($el.choiceButton -eq $false) { X "$inner<ChoiceButton>false</ChoiceButton>" }
elseif ($el.choiceButton -eq $true -and ($el.on -contains 'StartChoice')) { X "$inner<ChoiceButton>true</ChoiceButton>" }
if ($el.clearButton -eq $true) { X "$inner<ClearButton>true</ClearButton>" }
if ($el.spinButton -eq $true) { X "$inner<SpinButton>true</SpinButton>" }
if ($el.dropListButton -eq $true) { X "$inner<DropListButton>true</DropListButton>" }
if ($el.markIncomplete -eq $true) { X "$inner<AutoMarkIncomplete>true</AutoMarkIncomplete>" }
if ($el.textEdit -eq $false) { X "$inner<TextEdit>false</TextEdit>" }
if ($el.skipOnInput -eq $true) { X "$inner<SkipOnInput>true</SkipOnInput>" }
$hasAmw = $el.PSObject.Properties.Name -contains 'autoMaxWidth'
if ($hasAmw) {
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
} elseif ($el.multiLine -eq $true) {
X "$inner<AutoMaxWidth>false</AutoMaxWidth>"
}
if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
if ($el.horizontalStretch -eq $true) { X "$inner<HorizontalStretch>true</HorizontalStretch>" }
if ($el.verticalStretch -eq $true) { X "$inner<VerticalStretch>true</VerticalStretch>" }
if ($el.inputHint) {
Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner
}
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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`">"
$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
$tl = if ($el.titleLocation) { "$($el.titleLocation)" } else { "Right" }
X "$inner<TitleLocation>$tl</TitleLocation>"
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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"
}
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")
# 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 = "" }
}
# 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.
$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>"
}
function Emit-Radio {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<RadioButtonField name=`"$name`" id=`"$id`">"
$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
# TitleLocation default is None for radio (matches typical configurator behavior)
$tl = if ($el.titleLocation) {
switch ("$($el.titleLocation)") {
"none" { "None" }
"left" { "Left" }
"right" { "Right" }
"top" { "Top" }
"bottom" { "Bottom" }
default { "$($el.titleLocation)" }
}
} else { "None" }
X "$inner<TitleLocation>$tl</TitleLocation>"
# 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>"
}
# ChoiceList
if ($el.choiceList -and $el.choiceList.Count -gt 0) {
X "$inner<ChoiceList>"
$itemIndent = "$inner`t"
foreach ($item in $el.choiceList) {
# Pull value (and tolerate Russian synonym "значение")
$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."значение" }
}
# Pull presentation (presentation OR title synonym)
$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 }
}
$norm = Normalize-ChoiceValue -value $valRaw
# Auto-derive presentation if missing
if (-not $hasPres) {
if ($norm.XsiType -eq "xr:DesignTimeRef") {
$tail = ($norm.Text -split '\.')[-1]
$presRaw = Title-FromName -name $tail
} elseif ($norm.XsiType -eq "xs:string") {
$presRaw = $norm.Text
} 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<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
X "$valIndent</xr:Value>"
X "$itemIndent</xr:Item>"
}
X "$inner</ChoiceList>"
}
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio"
X "$indent</RadioButtonField>"
}
function Emit-Label {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<LabelDecoration name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
$labelTitle = if ($el.title) { "$($el.title)" } else { Title-FromName -name $name }
if ($labelTitle) {
$formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" }
X "$inner<Title formatted=`"$formatted`">"
X "$inner`t<v8:item>"
X "$inner`t`t<v8:lang>ru</v8:lang>"
X "$inner`t`t<v8:content>$(Esc-Xml "$labelTitle")</v8:content>"
X "$inner`t</v8:item>"
X "$inner</Title>"
}
Emit-CommonFlags -el $el -indent $inner
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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`">"
$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.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField"
X "$indent</LabelField>"
}
function Emit-Table {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<Table name=`"$name`" id=`"$id`">"
$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.changeRowSet -eq $true) { X "$inner<ChangeRowSet>true</ChangeRowSet>" }
if ($el.changeRowOrder -eq $true) { X "$inner<ChangeRowOrder>true</ChangeRowOrder>" }
if ($el.height) { X "$inner<HeightInTableRows>$($el.height)</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>" }
if ($el.initialTreeView) { X "$inner<InitialTreeView>$($el.initialTreeView)</InitialTreeView>" }
if ($el.enableStartDrag -eq $true) { X "$inner<EnableStartDrag>true</EnableStartDrag>" }
if ($el.enableDrag -eq $true) { X "$inner<EnableDrag>true</EnableDrag>" }
if ($el.rowPictureDataPath) { X "$inner<RowPictureDataPath>$($el.rowPictureDataPath)</RowPictureDataPath>" }
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
# AutoCommandBar — with optional Autofill control
if ($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 "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner
Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner
Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner
# 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`">"
$inner = "$indent`t"
if ($el.pagesRepresentation) {
X "$inner<PagesRepresentation>$($el.pagesRepresentation)</PagesRepresentation>"
}
Emit-CommonFlags -el $el -indent $inner
# Companion
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
if ($el.group) {
$orientation = switch ("$($el.group)") {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"alwaysHorizontal" { "AlwaysHorizontal" }
"alwaysVertical" { "AlwaysVertical" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
}
# Companion
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
# 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`">"
$inner = "$indent`t"
# 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>"
}
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>"
}
}
$btnAuto = -not ($el.command -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>" }
# Picture
if ($el.picture) {
X "$inner<Picture>"
X "$inner`t<xr:Ref>$($el.picture)</xr:Ref>"
X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
X "$inner</Picture>"
}
if ($el.representation) {
X "$inner<Representation>$($el.representation)</Representation>"
}
if ($el.locationInCommandBar) {
X "$inner<LocationInCommandBar>$($el.locationInCommandBar)</LocationInCommandBar>"
}
# Companion
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
Emit-CommonFlags -el $el -indent $inner
if ($el.picture -or $el.src) {
$ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" }
X "$inner<Picture>"
X "$inner`t<xr:Ref>$ref</xr:Ref>"
X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
X "$inner</Picture>"
}
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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`">"
$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
# ValuesPicture — picture (collection) used to render the field's value.
# Required for a Boolean-bound PictureField to actually show an icon.
# loadTransparent emitted only when true (1С default is false).
if ($el.valuesPicture) {
X "$inner<ValuesPicture>"
X "$inner`t<xr:Ref>$($el.valuesPicture)</xr:Ref>"
if ($el.loadTransparent) { X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>" }
X "$inner</ValuesPicture>"
}
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
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`">"
$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
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar"
X "$indent</CalendarField>"
}
function Emit-CommandBar {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<CommandBar name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
if ($el.autofill -eq $true) { X "$inner<Autofill>true</Autofill>" }
Emit-CommonFlags -el $el -indent $inner
# 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-Popup {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<Popup name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
if ($el.picture) {
X "$inner<Picture>"
X "$inner`t<xr:Ref>$($el.picture)</xr:Ref>"
X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
X "$inner</Picture>"
}
if ($el.representation) {
X "$inner<Representation>$($el.representation)</Representation>"
}
# 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 ---
function Emit-Attributes {
param($attrs, [string]$indent)
if (-not $attrs -or $attrs.Count -eq 0) { return }
X "$indent<Attributes>"
foreach ($attr in $attrs) {
$attrId = New-Id
$attrName = "$($attr.name)"
X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
$inner = "$indent`t`t"
$attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' }
if ($attrTitle) {
Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner
}
# Type
if ($attr.type) {
Emit-Type -typeStr "$($attr.type)" -indent $inner
} else {
X "$inner<Type/>"
}
if ($attr.main -eq $true) {
X "$inner<MainAttribute>true</MainAttribute>"
}
$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\.'
}
if ($attr.savedData -eq $true -or $mainSaved) {
X "$inner<SavedData>true</SavedData>"
}
if ($attr.fillChecking) {
X "$inner<FillChecking>$($attr.fillChecking)</FillChecking>"
}
# Columns (for ValueTable/ValueTree)
if ($attr.columns -and $attr.columns.Count -gt 0) {
X "$inner<Columns>"
foreach ($col in $attr.columns) {
$colId = New-Id
X "$inner`t<Column name=`"$($col.name)`" id=`"$colId`">"
if ($col.title) {
Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t"
}
Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t"
X "$inner`t</Column>"
}
X "$inner</Columns>"
}
# Settings (for DynamicList)
if ($attr.settings) {
X "$inner<Settings xsi:type=`"DynamicList`">"
$si = "$inner`t"
if ($attr.settings.mainTable) { X "$si<MainTable>$($attr.settings.mainTable)</MainTable>" }
$mq = if ($attr.settings.manualQuery -eq $true) { "true" } else { "false" }
X "$si<ManualQuery>$mq</ManualQuery>"
$ddr = if ($attr.settings.dynamicDataRead -eq $true) { "true" } else { "false" }
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
X "$inner</Settings>"
}
X "$indent`t</Attribute>"
}
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>"
foreach ($param in $params) {
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>"
foreach ($cmd in $cmds) {
$cmdId = New-Id
X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
$inner = "$indent`t`t"
$cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" }
if ($cmdTitle) {
Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner
}
if ($cmd.action) {
X "$inner<Action>$($cmd.action)</Action>"
}
if ($cmd.shortcut) {
X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>"
}
if ($cmd.picture) {
X "$inner<Picture>"
X "$inner`t<xr:Ref>$($cmd.picture)</xr:Ref>"
X "$inner`t<xr:LoadTransparent>true</xr:LoadTransparent>"
X "$inner</Picture>"
}
if ($cmd.representation) {
X "$inner<Representation>$($cmd.representation)</Representation>"
}
X "$indent`t</Command>"
}
X "$indent</Commands>"
}
# --- 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
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 ---
function Normalize-ElementSynonyms {
param($el)
if ($null -eq $el) { return }
$synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -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["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) {
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
}
# DefaultPicture доступен только если у DynamicList есть основная таблица
if ($hasMainTable -and ($null -eq $el.PSObject.Properties["rowPictureDataPath"] -or [string]::IsNullOrEmpty("$($el.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
if ($def.attributes -and $def.elements) {
$mainAttr = $null
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $mainAttr = $attr; break }
}
if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") {
$mt = $null
if ($mainAttr.PSObject.Properties["settings"] -and $null -ne $mainAttr.settings) {
if ($mainAttr.settings -is [hashtable]) {
if ($mainAttr.settings.ContainsKey("mainTable")) { $mt = $mainAttr.settings["mainTable"] }
} elseif ($mainAttr.settings.PSObject.Properties["mainTable"]) {
$mt = $mainAttr.settings.mainTable
}
}
$hasMt = -not [string]::IsNullOrEmpty("$mt")
foreach ($el in $def.elements) {
ApplyDynamicListTableHeuristic $el $mainAttr.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
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>"
}
# 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)
$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren)
if ($acbHasInner) {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">"
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`"/>"
}
# 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"
# 12h. Parameters
Emit-Parameters -params $def.parameters -indent "`t"
# 12i. Commands
Emit-Commands -cmds $def.commands -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)" }