# meta-compile v1.65 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$JsonPath,
[Parameter(Mandatory)]
[string]$OutputDir
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- 1. Load and validate JSON ---
if (-not (Test-Path $JsonPath)) {
Write-Error "File not found: $JsonPath"
exit 1
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
Assert-EditAllowed $OutputDir 'editable'
# --- Batch mode: JSON array of objects ---
if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) {
$batchOk = 0
$batchFail = 0
$idx = 0
foreach ($item in $def) {
$idx++
$tmpJson = Join-Path ([System.IO.Path]::GetTempPath()) "meta-compile-batch-$idx.json"
try {
$item | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpJson
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$PSCommandPath`" -JsonPath `"$tmpJson`" -OutputDir `"$OutputDir`"" -NoNewWindow -Wait -PassThru
if ($proc.ExitCode -eq 0) { $batchOk++ } else { $batchFail++ }
} finally {
Remove-Item $tmpJson -Force -ErrorAction SilentlyContinue
}
}
Write-Host ""
Write-Host "=== Batch: $idx objects, $batchOk compiled, $batchFail failed ==="
if ($batchFail -gt 0) { exit 1 }
exit 0
}
# Normalize field synonyms: accept "objectType" as alias for "type"
if (-not $def.type -and $def.objectType) {
$def | Add-Member -NotePropertyName "type" -NotePropertyValue $def.objectType
}
# Object type synonyms (Russian → English)
$script:objectTypeSynonyms = @{
"Справочник" = "Catalog"
"Каталог" = "Catalog"
"Документ" = "Document"
"Перечисление" = "Enum"
"Константа" = "Constant"
"РегистрСведений" = "InformationRegister"
"РегистрНакопления" = "AccumulationRegister"
"РегистрБухгалтерии" = "AccountingRegister"
"РегистрРасчёта" = "CalculationRegister"
"РегистрРасчета" = "CalculationRegister"
"ПланСчетов" = "ChartOfAccounts"
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
"ПланВидовРасчёта" = "ChartOfCalculationTypes"
"ПланВидовРасчета" = "ChartOfCalculationTypes"
"БизнесПроцесс" = "BusinessProcess"
"Задача" = "Task"
"ПланОбмена" = "ExchangePlan"
"ЖурналДокументов" = "DocumentJournal"
"Отчёт" = "Report"
"Отчет" = "Report"
"Обработка" = "DataProcessor"
"ОбщийМодуль" = "CommonModule"
"РегламентноеЗадание" = "ScheduledJob"
"ПодпискаНаСобытие" = "EventSubscription"
"HTTPСервис" = "HTTPService"
"ВебСервис" = "WebService"
"ОпределяемыйТип" = "DefinedType"
"ФункциональнаяОпция" = "FunctionalOption"
}
# Enum property value synonyms — model often gets these slightly wrong
$script:enumValueAliases = @{
# RegisterType (AccumulationRegister)
"Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers"
# WriteMode (InformationRegister)
"RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate"
"ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent"
# DependenceOnCalculationTypes (ChartOfCalculationTypes)
"NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse"
"Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod"
# InformationRegisterPeriodicity
"None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month"
"Quarterly" = "Quarter"; "Yearly" = "Year"
"Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"
"Месяц" = "Month"; "Квартал" = "Quarter"; "Год" = "Year"
"ПозицияРегистратора" = "RecorderPosition"
# DataLockControlMode
"Автоматический" = "Automatic"; "Управляемый" = "Managed"
# FullTextSearch
"Использовать" = "Use"; "НеИспользовать" = "DontUse"
# Posting
"Разрешить" = "Allow"; "Запретить" = "Deny"
# EditType
"ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays"
# DefaultPresentation
"ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode"
# FillChecking
"НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning"
# Indexing
"НеИндексировать" = "DontIndex"; "Индексировать" = "Index"
"ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder"
}
# Valid enum values per property (from meta-validate)
$script:validEnumValues = @{
"RegisterType" = @("Balance","Turnovers")
"WriteMode" = @("Independent","RecorderSubordinate")
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
"DataLockControlMode" = @("Automatic","Managed")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
"DefaultPresentation" = @("AsDescription","AsCode")
"Posting" = @("Allow","Deny")
"RealTimePosting" = @("Allow","Deny")
"EditType" = @("InDialog","InList","BothWays")
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyOfItems")
"CodeType" = @("String","Number")
"CodeAllowedLength" = @("Variable","Fixed")
"NumberType" = @("String","Number")
"NumberAllowedLength" = @("Variable","Fixed")
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
"ReturnValuesReuse" = @("DontUse","DuringRequest","DuringSession")
"ReuseSessions" = @("DontUse","AutoUse")
"FillChecking" = @("DontCheck","ShowError","ShowWarning")
"Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder")
"SubordinationUse" = @("ToItems","ToFolders","ToFoldersAndItems")
"CodeSeries" = @("WholeCatalog","WithinSubordination","WithinOwnerSubordination","WholeCharacteristicKind","WholeChartOfAccounts")
"ChoiceMode" = @("BothWays","QuickChoice","FromForm")
"CreateOnInput" = @("Auto","Use","DontUse")
"ChoiceHistoryOnInput" = @("Auto","DontUse")
"PredefinedDataUpdate" = @("Auto","DontAutoUpdate","AutoUpdate")
"SearchStringModeOnInputByString"= @("Begin","AnyPart")
"FullTextSearchOnInputByString" = @("Use","DontUse")
"Category" = @("NavigationPanel","ActionsPanel","FormCommandBar","FormNavigationPanel")
}
# --- Группы команд объекта (командный интерфейс) ---
# Группы командного интерфейса РАЗДЕЛА (панель навигации/действий): команда БЕЗ параметра. Группы формы: параметр доступен.
$script:sectionCommandGroups = @(
"NavigationPanelImportant","NavigationPanelOrdinary","NavigationPanelSeeAlso",
"ActionsPanelCreate","ActionsPanelReports","ActionsPanelTools"
)
$script:formCommandGroups = @(
"FormCommandBarImportant","FormCommandBarCreateBasedOn",
"FormNavigationPanelImportant","FormNavigationPanelGoTo","FormNavigationPanelSeeAlso"
)
$script:validCommandGroups = $script:sectionCommandGroups + $script:formCommandGroups
# Прощающий ввод: русские подписи групп → канон
$script:commandGroupAliases = @{
"Панель навигации.Важное" = "NavigationPanelImportant"
"Панель навигации.Обычное" = "NavigationPanelOrdinary"
"Панель навигации.См. также" = "NavigationPanelSeeAlso"
"Панель действий.Создать" = "ActionsPanelCreate"
"Панель действий.Отчеты" = "ActionsPanelReports"
"Панель действий.Отчёты" = "ActionsPanelReports"
"Панель действий.Сервис" = "ActionsPanelTools"
"Командная панель формы.Важное" = "FormCommandBarImportant"
"Командная панель формы.Создать на основании" = "FormCommandBarCreateBasedOn"
"Панель навигации формы.Важное" = "FormNavigationPanelImportant"
"Панель навигации формы.Перейти" = "FormNavigationPanelGoTo"
"Панель навигации формы.См. также" = "FormNavigationPanelSeeAlso"
}
# Резолв группы команды: рус-синоним → канон; ГруппаКоманд.X → CommandGroup.X; пусто → ошибка с подсказкой.
function Resolve-CommandGroup {
param([string]$raw, [string]$cmdName)
$g = "$raw".Trim()
if (-not $g) {
Write-Error "Команде '$cmdName' не задана группа (group). 1С требует группу. Валидные: $($script:validCommandGroups -join ', '); либо CommandGroup.<Имя> — кастомная группа."
exit 1
}
if ($script:commandGroupAliases.ContainsKey($g)) { return $script:commandGroupAliases[$g] }
if ($g -match '^(?:CommandGroup|ГруппаКоманд)\.(.+)$') { return "CommandGroup.$($Matches[1])" }
return $g
}
function Normalize-EnumValue {
param([string]$propName, [string]$value)
# 1. Check alias dictionary — silent auto-correct
if ($script:enumValueAliases.ContainsKey($value)) {
return $script:enumValueAliases[$value]
}
# 2. Case-insensitive match against valid values — silent
$valid = $script:validEnumValues[$propName]
if ($valid) {
foreach ($v in $valid) {
if ($v -ieq $value) { return $v }
}
# 3. Known property, unknown value — error with hint
Write-Error "Invalid value '$value' for property '$propName'. Valid values: $($valid -join ', ')"
exit 1
}
# 4. Unknown property — pass-through (no validation data)
return $value
}
# Helper: read enum property from $def with default and normalization
function Get-EnumProp {
param([string]$propName, [string]$fieldName, [string]$default)
$val = $def.$fieldName
$raw = if ($val) { "$val" } else { $default }
return (Normalize-EnumValue $propName $raw)
}
# Bool object-свойство: presence-aware (иначе false-значение спутать с отсутствием). Прощаем строки.
function Get-BoolProp {
param([string]$fieldName, [bool]$default)
$val = $def.$fieldName
if ($null -eq $val) { return $default }
if ($val -is [bool]) { return $val }
return ("$val" -match '^(true|1|да|истина)$')
}
# Прощающая нормализация ссылки на форму: рус корень (Справочник→Catalog), сегмент Форма→Form,
# короткая запись "Тип.Объект.ИмяФормы" (без Form) → вставка Form. Уже канон англ. → без изменений.
function Normalize-FormRef {
param([string]$s)
if (-not $s) { return $s }
$parts = $s -split '\.'
if ($parts.Count -lt 3) { return $s }
$root = $script:fillRefRoots[$parts[0].ToLower()]
if ($root) { $parts[0] = $root }
for ($k = 1; $k -lt $parts.Count; $k++) { if ($parts[$k] -ieq 'Форма') { $parts[$k] = 'Form' } }
if (($parts -notcontains 'Form') -and $parts.Count -eq 3) { $parts = @($parts[0], $parts[1], 'Form', $parts[2]) }
return ($parts -join '.')
}
# Ссылка на форму по умолчанию: непустая → значение, иначе .
function Emit-FormRef {
param([string]$i, [string]$tag, $val)
if ($val) { X "$i<$tag>$(Esc-Xml (Normalize-FormRef "$val"))$tag>" } else { X "$i<$tag/>" }
}
# Ссылка verbatim (без Normalize-FormRef): для форм/схем/хранилищ Report/DataProcessor, где имя формы может быть
# буквально «Форма» (Normalize-FormRef перевёл бы имя-сегмент Форма→Form, испортив ссылку) либо ref не-форменного
# вида (SettingsStorage.X / Report.X.Template.Y). Декомпилятор пишет полный путь → passthrough.
function Emit-VerbatimRef {
param([string]$i, [string]$tag, $val)
if ($val) { X "$i<$tag>$(Esc-Xml "$val")$tag>" } else { X "$i<$tag/>" }
}
if (-not $def.type) {
Write-Error "JSON must have 'type' field"
exit 1
}
# Resolve type synonym
$objType = "$($def.type)"
if ($script:objectTypeSynonyms.ContainsKey($objType)) {
$objType = $script:objectTypeSynonyms[$objType]
}
$validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","AccumulationRegister",
"AccountingRegister","CalculationRegister","ChartOfAccounts","ChartOfCharacteristicTypes",
"ChartOfCalculationTypes","BusinessProcess","Task","ExchangePlan","DocumentJournal",
"Report","DataProcessor","CommonModule","ScheduledJob","EventSubscription",
"HTTPService","WebService","DefinedType","FunctionalOption",
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
"CommonPicture","CommonTemplate")
if ($objType -notin $validTypes) {
Write-Error "Unsupported type: $objType. Valid: $($validTypes -join ', ')"
exit 1
}
if (-not $def.name) {
Write-Error "JSON must have 'name' field"
exit 1
}
$objName = "$($def.name)"
# --- 2. XML helpers ---
$script:xml = New-Object System.Text.StringBuilder 32768
function X {
param([string]$text)
$script:xml.AppendLine($text) | Out-Null
}
function Esc-Xml {
param([string]$s)
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
}
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw, экранирование только для атрибутов).
function Esc-XmlText {
param([string]$s)
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
}
# ML-значение: строка → один ru; объект {lang: content} → item на язык (в порядке ключей).
function Emit-MLItems {
param([string]$indent, $val)
if ($val -is [System.Collections.IDictionary]) {
foreach ($k in $val.Keys) {
X "$indent"; X "$indent`t$k"; X "$indent`t$(Esc-XmlText "$($val[$k])")"; X "$indent"
}
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
foreach ($p in $val.PSObject.Properties) {
X "$indent"; X "$indent`t$($p.Name)"; X "$indent`t$(Esc-XmlText "$($p.Value)")"; X "$indent"
}
} else {
X "$indent"; X "$indent`tru"; X "$indent`t$(Esc-XmlText "$val")"; X "$indent"
}
}
function Emit-MLText {
param([string]$indent, [string]$tag, $text)
# Пусто (null / пустая строка) → самозакрывающийся тег.
if (($null -eq $text) -or (($text -is [string]) -and ($text -eq ''))) {
X "$indent<$tag/>"
return
}
X "$indent<$tag>"
Emit-MLItems "$indent`t" $text
X "$indent$tag>"
}
function New-Guid-String {
return [System.Guid]::NewGuid().ToString()
}
# --- 3. CamelCase splitter ---
function Split-CamelCase {
param([string]$name)
if (-not $name) { return $name }
# Insert space before uppercase that follows lowercase (Cyrillic + Latin)
$result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2')
$result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2')
# Лоуэркейзим хвост, СОХРАНЯЯ аббревиатуры (зеркало эвристики платформы): максимальный прогон
# заглавных длиной >=2, если сразу за ним НЕ буква (пробел/цифра/спецсимвол/конец) — остаётся заглавным
# (НДС, ЕГАИС, ОС, ЭП). Прилипшие предлоги (СКлиентами) и бренды (ЮКасса) идут перед буквой → лоуэркейз.
# Первый символ строки — как есть.
if ($result.Length -gt 1) {
$chars = $result.ToCharArray()
$n = $chars.Length
$keep = New-Object 'bool[]' $n
$i = 0
while ($i -lt $n) {
if ([char]::IsUpper($chars[$i])) {
$j = $i
while ($j -lt $n -and [char]::IsUpper($chars[$j])) { $j++ }
$afterBoundary = ($j -eq $n) -or (-not [char]::IsLetter($chars[$j]))
if (($j - $i) -ge 2 -and $afterBoundary) { for ($k = $i; $k -lt $j; $k++) { $keep[$k] = $true } }
$i = $j
} else { $i++ }
}
$sb = New-Object System.Text.StringBuilder
for ($idx = 0; $idx -lt $n; $idx++) {
$c = $chars[$idx]
if ($idx -eq 0 -or $keep[$idx]) { [void]$sb.Append($c) }
elseif ([char]::IsUpper($c)) { [void]$sb.Append([char]::ToLower($c)) }
else { [void]$sb.Append($c) }
}
$result = $sb.ToString()
}
return $result
}
# Auto-synonym. Проброс без стрингификации (строка ИЛИ {ru,en} — мультиязычный синоним объекта).
$synonym = if ($null -ne $def.synonym) { $def.synonym } else { Split-CamelCase $objName }
$comment = if ($def.comment) { "$($def.comment)" } else { "" }
# --- 4. Type system ---
$script:typeSynonyms = New-Object System.Collections.Hashtable
$script:typeSynonyms["число"] = "Number"
$script:typeSynonyms["строка"] = "String"
$script:typeSynonyms["булево"] = "Boolean"
$script:typeSynonyms["дата"] = "Date"
$script:typeSynonyms["датавремя"]= "DateTime"
$script:typeSynonyms["время"] = "Time"
$script:typeSynonyms["time"] = "Time"
$script:typeSynonyms["number"] = "Number"
$script:typeSynonyms["string"] = "String"
$script:typeSynonyms["boolean"] = "Boolean"
$script:typeSynonyms["date"] = "Date"
$script:typeSynonyms["datetime"] = "DateTime"
$script:typeSynonyms["bool"] = "Boolean"
# ValueStorage / UUID — прощающий ввод (модель может написать base64Binary / рус. форму → канон).
$script:typeSynonyms["valuestorage"] = "ValueStorage"
$script:typeSynonyms["base64binary"] = "ValueStorage"
$script:typeSynonyms["хранилищезначений"] = "ValueStorage"
$script:typeSynonyms["хранилищезначения"] = "ValueStorage"
$script:typeSynonyms["uuid"] = "UUID"
$script:typeSynonyms["уникальныйидентификатор"] = "UUID"
# Платформенные типы, требующие префикса v8: (коллекции/периоды, частые в реквизитах обработок/отчётов).
$script:v8PlatformTypes = @("ValueTable","ValueTree","ValueList","ValueListType","StandardPeriod",
"StandardBeginningDate","PointInTime","TypeDescription","FixedArray","FixedMap","FixedStructure")
# Типы со ВЫДЕЛЕННЫМ пространством имён (локальный xmlns на ). prefix — канон корпуса
# (dcsset/mxl — семантические из корневых деклараций 1С; chart — генерируемый dNpM, любой подойдёт).
$script:typeNamespaceMap = @{
"Chart" = @{ ns = "http://v8.1c.ru/8.2/data/chart"; prefix = "d5p1" }
"SettingsComposer" = @{ ns = "http://v8.1c.ru/8.1/data-composition-system/settings"; prefix = "dcsset" }
"SpreadsheetDocument" = @{ ns = "http://v8.1c.ru/8.2/data/spreadsheet"; prefix = "mxl" }
}
# Типы current-config пространства (cfg:, объявлено в корне): объектные (CatalogObject.X/DataProcessorObject.X/…)
# и голые (ConstantsSet/ReportBuilder). Ссылочные (*Ref.X/DefinedType.X) идут ОТДЕЛЬНО через локальный d5p1 (§memory).
$script:cfgBareTypes = @("ConstantsSet", "ReportBuilder", "FilterCriterion")
$script:cfgObjectKinds = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes",
"ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task","InformationRegister","AccumulationRegister",
"AccountingRegister","CalculationRegister","DataProcessor","Report","DocumentJournal","Constant","ConstantValue","Sequence","Recalculation")
$script:typeSynonyms["таблицазначений"] = "ValueTable"
$script:typeSynonyms["деревозначений"] = "ValueTree"
$script:typeSynonyms["списокзначений"] = "ValueListType"
$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod"
# Reference synonyms (Russian, lowercase)
$script:typeSynonyms["справочникссылка"] = "CatalogRef"
$script:typeSynonyms["документссылка"] = "DocumentRef"
$script:typeSynonyms["перечислениессылка"] = "EnumRef"
$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef"
$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef"
$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef"
$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef"
$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef"
$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef"
$script:typeSynonyms["задачассылка"] = "TaskRef"
$script:typeSynonyms["определяемыйтип"] = "DefinedType"
$script:typeSynonyms["definedtype"] = "DefinedType"
# English lowercase ref synonyms
$script:typeSynonyms["catalogref"] = "CatalogRef"
$script:typeSynonyms["documentref"] = "DocumentRef"
$script:typeSynonyms["enumref"] = "EnumRef"
function Resolve-TypeStr {
param([string]$typeStr)
if (-not $typeStr) { return $typeStr }
# Check for parameterized types: Number(15,2), Строка(100), etc.
if ($typeStr -match '^([^(]+)\((.+)\)$') {
$baseName = $Matches[1].Trim()
$params = $Matches[2]
$resolved = $script:typeSynonyms[$baseName.ToLower()]
if ($resolved) { return "$resolved($params)" }
return $typeStr
}
# Check for reference types: СправочникСсылка.Организации → CatalogRef.Организации
if ($typeStr.Contains('.')) {
$dotIdx = $typeStr.IndexOf('.')
$prefix = $typeStr.Substring(0, $dotIdx)
$suffix = $typeStr.Substring($dotIdx) # includes the dot
$resolved = $script:typeSynonyms[$prefix.ToLower()]
if ($resolved) { return "$resolved$suffix" }
return $typeStr
}
# Simple name lookup
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
if ($resolved) { return $resolved }
return $typeStr
}
function Emit-TypeContent {
param([string]$indent, [string]$typeStr)
if (-not $typeStr) { return }
# Composite type: "Type1 + Type2 + Type3"
if ($typeStr.Contains(' + ')) {
$parts = $typeStr -split '\s*\+\s*'
foreach ($part in $parts) {
Emit-TypeContent $indent $part.Trim()
}
return
}
$typeStr = Resolve-TypeStr $typeStr
# Boolean
if ($typeStr -eq "Boolean") {
X "$indentxs:boolean"
return
}
# String or String(N) or String(N,fixed|variable) — AllowedLength: Variable дефолт / Fixed (фикс. длина).
if ($typeStr -match '^String(\((\d+)(\s*,\s*(fixed|variable))?\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "10" }
$al = if ($Matches[4] -and $Matches[4].ToLower() -eq 'fixed') { 'Fixed' } else { 'Variable' }
X "$indentxs:string"
X "$indent"
X "$indent`t$len"
X "$indent`t$al"
X "$indent"
return
}
# Number without params → Number(10,0)
if ($typeStr -eq "Number") {
X "$indentxs:decimal"
X "$indent"
X "$indent`t10"
X "$indent`t0"
X "$indent`tAny"
X "$indent"
return
}
# Number(D,F) or Number(D,F,nonneg)
if ($typeStr -match '^Number\((\d+),(\d+)(,nonneg)?\)$') {
$digits = $Matches[1]
$fraction = $Matches[2]
$sign = if ($Matches[3]) { "Nonnegative" } else { "Any" }
X "$indentxs:decimal"
X "$indent"
X "$indent`t$digits"
X "$indent`t$fraction"
X "$indent`t$sign"
X "$indent"
return
}
# Date / DateTime / Time — общая структура xs:dateTime + DateFractions (различаются лишь составом).
if ($typeStr -match '^(Date|DateTime|Time)$') {
X "$indentxs:dateTime"
X "$indent"
X "$indent`t$typeStr"
X "$indent"
return
}
# TypeSet — тип-множество: ОпределяемыйТип (DefinedType) ИЛИ Характеристика ПВХ (Characteristic).
if ($typeStr -match '^(DefinedType|Characteristic)\.(.+)$') {
X "$indentcfg:$typeStr"
return
}
# Голый метатип-категория (CatalogRef/DocumentRef/…/AnyRef/AnyIBRef без имени объекта) — множество
# «любой объект категории» → TypeSet (а не конкретный Type с именем).
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$') {
X "$indentcfg:$typeStr"
return
}
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба).
if ($typeStr -eq "ValueStorage") {
X "$indentv8:ValueStorage"
return
}
# UUID (УникальныйИдентификатор)
if ($typeStr -eq "UUID") {
X "$indentv8:UUID"
return
}
# Платформенные типы-коллекции/периоды (ТаблицаЗначений/ДеревоЗначений/СписокЗначений/StandardPeriod/…) —
# канон с префиксом v8: (декомпилятор снимает префикс через Strip-NsPrefix, компилятор возвращает).
if ($script:v8PlatformTypes -contains $typeStr) {
X "$indentv8:$typeStr"
return
}
# Типы с выделенным пространством имён (Chart/SettingsComposer/SpreadsheetDocument) — локальный xmlns.
if ($script:typeNamespaceMap.ContainsKey($typeStr)) {
$m = $script:typeNamespaceMap[$typeStr]
X "$indent$($m.prefix):$typeStr"
return
}
# Типы current-config (cfg:): голые (ConstantsSet/…) и объектные (CatalogObject.X/DataProcessorObject.X/…).
if ($script:cfgBareTypes -contains $typeStr) {
X "$indentcfg:$typeStr"
return
}
if ($typeStr -match '^(\w+)(Object|List|Manager|Selection|RecordSet|RecordKey|RecordManager)\.(.+)$' -and $script:cfgObjectKinds -contains $Matches[1]) {
X "$indentcfg:$typeStr"
return
}
# Голый объектный метатип (без имени) — напр. в Source подписки на событие:
# - Object/RecordSet → «любой объект категории» = TypeSet cfg: (множество);
# - Manager/List/Selection/RecordKey/RecordManager → сам тип менеджера/списка = Type cfg: (единичный).
# ConstantValueManager (голый) — исключение: множество менеджеров значений констант = TypeSet (прочие *Manager → Type).
if (($typeStr -match '^(\w+)(Object|RecordSet)$' -and $script:cfgObjectKinds -contains $Matches[1]) -or $typeStr -eq 'ConstantValueManager') {
X "$indentcfg:$typeStr"
return
}
if ($typeStr -match '^(\w+)(Manager|List|Selection|RecordKey|RecordManager)$' -and $script:cfgObjectKinds -contains $Matches[1]) {
X "$indentcfg:$typeStr"
return
}
# Reference types — use local xmlns declaration for 1C compatibility
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
X "$indentd5p1:$typeStr"
return
}
# Fallback — emit as-is
X "$indent$typeStr"
}
function Emit-ValueType {
param([string]$indent, [string]$typeStr)
X "$indent"
Emit-TypeContent "$indent`t" $typeStr
X "$indent"
}
# --- FillValue (значение заполнения реквизита) ---
# Пара FillFromFillingValue+FillValue — единый блок «заполнения» (недоступен у реквизитов ТЧ).
# Форма пустого FillValue зависит от типа реквизита (то же значение по умолчанию, что и «пустое»
# значение типа): String→typed-empty, Number→0, всё остальное (Boolean/Date/Ref/составной/TypeSet)→nil.
# Реальное значение задаётся ключом `fillValue` (интерпретация по типу реквизита; см. §4.2 spec).
# Категория типа реквизита для выбора формы FillValue.
function Get-FillTypeCategory {
param([string]$typeStr)
if (-not $typeStr) { return 'String' } # реквизит без типа → неквалифиц. строка
if ($typeStr -match '\+') { return 'Other' } # составной тип → nil-дефолт
$t = Resolve-TypeStr $typeStr
if ($t -match '^Boolean$') { return 'Boolean' }
if ($t -match '^String(\(|$)') { return 'String' }
if ($t -match '^Number(\(|$)') { return 'Number' }
if ($t -match '^(Date|DateTime)$') { return 'Date' }
return 'Other' # ссылки, TypeSet, ValueStorage, … → nil-дефолт
}
# Прощающий ввод для ссылочных путей DTR: рус/англ корни, ПустаяСсылка/EmptyRef, ЗначениеПеречисления/EnumValue.
$script:fillRefRoots = @{
'перечисление'='Enum'; 'справочник'='Catalog'; 'документ'='Document';
'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
'enum'='Enum'; 'catalog'='Catalog'; 'document'='Document'; 'chartofaccounts'='ChartOfAccounts';
'chartofcharacteristictypes'='ChartOfCharacteristicTypes'; 'chartofcalculationtypes'='ChartOfCalculationTypes';
'exchangeplan'='ExchangePlan'; 'businessprocess'='BusinessProcess'; 'task'='Task'
}
$script:fillEmptyRefWords = @('emptyref','пустаяссылка')
$script:fillEnumValWords = @('enumvalue','значениеперечисления')
$script:fillBoolTrue = @('true','истина','да')
$script:fillBoolFalse = @('false','ложь','нет')
# Прощающий ввод MDObjectRef-путей (Location/Content функц. опции, registerRecords и т.п.): русские корни
# метаданных + подвиды → английские. Виды стоят на ЧЁТНЫХ позициях (0,2,4…), имена (нечётные) не трогаем.
# Английские пути проходят без изменений (в мапе только русские ключи) → роундтрип byte-exact сохраняется.
$script:mdRefRoots = @{
'справочник'='Catalog'; 'документ'='Document'; 'перечисление'='Enum'; 'константа'='Constant';
'регистрсведений'='InformationRegister'; 'регистрнакопления'='AccumulationRegister';
'регистрбухгалтерии'='AccountingRegister'; 'регистррасчета'='CalculationRegister'; 'регистррасчёта'='CalculationRegister';
'плансчетов'='ChartOfAccounts'; 'планвидовхарактеристик'='ChartOfCharacteristicTypes';
'планвидоврасчета'='ChartOfCalculationTypes'; 'планвидоврасчёта'='ChartOfCalculationTypes';
'планобмена'='ExchangePlan'; 'бизнеспроцесс'='BusinessProcess'; 'задача'='Task';
'журналдокументов'='DocumentJournal'; 'отчет'='Report'; 'отчёт'='Report'; 'обработка'='DataProcessor';
'табличнаячасть'='TabularSection'; 'реквизит'='Attribute'; 'измерение'='Dimension'; 'ресурс'='Resource';
'стандартныйреквизит'='StandardAttribute'; 'значениеперечисления'='EnumValue'; 'команда'='Command';
'признакучета'='AccountingFlag'; 'признакучёта'='AccountingFlag'
}
function Normalize-MDObjectRef {
param([string]$ref)
if (-not $ref -or -not $ref.Contains('.')) { return $ref }
$parts = $ref -split '\.'
for ($k = 0; $k -lt $parts.Count; $k += 2) {
$t = $script:mdRefRoots[$parts[$k].ToLower()]
if ($t) { $parts[$k] = $t }
}
return ($parts -join '.')
}
# Значения платформенного перечисления ВидСчета (ent:AccountType) — FillValue стандартного реквизита Тип у Плана счетов.
$script:accountTypeValues = @('Active','Passive','ActivePassive')
# XxxRef (тип реквизита) → корень DTR-пути (для разворота короткой записи значения).
$script:fillRefKindRoot = @{
'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum';
'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes';
'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan';
'businessprocessref'='BusinessProcess'; 'taskref'='Task'
}
# Короткая запись значения ссылочного реквизита (без точки): имя разворачиваем по типу реквизита.
# "EmptyRef"/"ПустаяСсылка" → .<Тип>.EmptyRef; для Enum — EnumValue; прочие — предопределённое.
# $null, если развернуть нельзя (тип не одиночный ссылочный).
function Expand-FillShortRef {
param([string]$s, [string]$typeStr)
if (-not $typeStr) { return $null }
if ($typeStr -match '\+') { return $null } # составной тип — короткая форма неоднозначна
$t = Resolve-TypeStr $typeStr
if ($t -notmatch '^(\w+Ref)\.(.+)$') { return $null }
$root = $script:fillRefKindRoot[$Matches[1].ToLower()]
if (-not $root) { return $null }
$typeName = $Matches[2]
if ($script:fillEmptyRefWords -contains $s.ToLower()) { return "$root.$typeName.EmptyRef" }
if ($root -eq 'Enum') { return "Enum.$typeName.EnumValue.$s" }
return "$root.$typeName.$s"
}
# Строка → нормализованный DTR-путь ("Catalog.X.EmptyRef" / "Enum.X.EnumValue.Y" / GUID.GUID) ЛИБО $null (не ссылка).
function Normalize-FillRef {
param([string]$s)
if ([string]::IsNullOrEmpty($s)) { return $null }
# Raw-ссылка по паре GUID (метаданные.значение) — всегда ссылка.
if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.[0-9a-fA-F-]+$') { return $s }
$parts = $s -split '\.'
if ($parts.Count -lt 2) { return $null }
$root = $script:fillRefRoots[$parts[0].ToLower()]
if (-not $root) { return $null }
$typeName = $parts[1]
if ($root -eq 'Enum') {
if ($parts.Count -eq 2) { return $null } # "Enum.X" — не значение
if ($parts.Count -eq 3) {
if ($script:fillEmptyRefWords -contains $parts[2].ToLower()) { return "Enum.$typeName.EmptyRef" }
return "Enum.$typeName.EnumValue.$($parts[2])"
}
$member = $parts[2]
if ($script:fillEnumValWords -contains $member.ToLower()) { $rest = $parts[3..($parts.Count-1)] -join '.' }
else { $rest = $parts[2..($parts.Count-1)] -join '.' }
return "Enum.$typeName.EnumValue.$rest"
}
# Прочие корни: переводим корень, ПустаяСсылка→EmptyRef в хвосте.
$tail = @($parts[1..($parts.Count-1)])
for ($i = 0; $i -lt $tail.Count; $i++) {
if ($script:fillEmptyRefWords -contains $tail[$i].ToLower()) { $tail[$i] = 'EmptyRef' }
}
return "$root." + ($tail -join '.')
}
# Строковый spec → @{ XsiType; Text }. Интерпретация по типу реквизита ($typeStr).
function Resolve-FillValueSpec {
param([string]$s, [string]$typeStr)
$cat = Get-FillTypeCategory $typeStr
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
# String-реквизит: значение заполнения — всегда строковый литерал (без ref/date-детекции).
if ($cat -eq 'String') { return @{ XsiType='xs:string'; Text=$s } }
# Булевы слова (для Boolean-реквизита ИЛИ явное истина/ложь).
if ($cat -eq 'Boolean' -or ($script:fillBoolTrue -contains $s.ToLower()) -or ($script:fillBoolFalse -contains $s.ToLower())) {
if ($script:fillBoolTrue -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='true' } }
if ($script:fillBoolFalse -contains $s.ToLower()) { return @{ XsiType='xs:boolean'; Text='false' } }
}
if ($cat -eq 'Number') { return @{ XsiType='xs:decimal'; Text=$s } }
# Дата: явный Date-реквизит ИЛИ ISO-паттерн. "2020-01-01" → добавить время.
if ($cat -eq 'Date' -or $s -match '^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?$') {
if ($s -match '^\d{4}-\d{2}-\d{2}$') { $s = "${s}T00:00:00" }
return @{ XsiType='xs:dateTime'; Text=$s }
}
# Полный ссылочный путь DTR (с точкой: "Catalog.X.EmptyRef", "Enum.X.EnumValue.Y", GUID.GUID).
$ref = Normalize-FillRef $s
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
# Короткая запись значения ссылочного реквизита (одно имя — разворачиваем по типу).
$short = Expand-FillShortRef $s $typeStr
if ($short) { return @{ XsiType='xr:DesignTimeRef'; Text=$short } }
# Фолбэк — строковый литерал.
return @{ XsiType='xs:string'; Text=$s }
}
# Формат числа-значения без привязки к культуре (точка-разделитель).
function Format-FillNum {
param($n)
if ($n -is [double] -or $n -is [decimal]) { return $n.ToString([System.Globalization.CultureInfo]::InvariantCulture) }
return "$n"
}
# $spec — значение ключа `fillValue` ($null при явном nil-override), $hasSpec — присутствует ли ключ.
function Emit-FillValue {
param([string]$indent, [string]$typeStr, $spec, $hasSpec, [bool]$typeEmpty = $false)
# Пустой (реквизит без типа) → форма пустого значения nil (как составной/ссылочный), НЕ xs:string:
# у бестипового реквизита нет типизированного «пустого» значения.
$cat = if ($typeEmpty) { 'Other' } else { Get-FillTypeCategory $typeStr }
if ($hasSpec -ne $true) {
# Значение не задано — форма по умолчанию для типа.
switch ($cat) {
'String' { X "$indent"; return }
'Number' { X "$indent0"; return }
default { X "$indent"; return }
}
}
if ($null -eq $spec) { X "$indent"; return } # явный nil-override
if ($spec.nil -eq $true) { X "$indent"; return } # явный nil на типизированном (маркер декомпилятора)
if ($spec.emptyRef -eq $true) { X "$indent"; return } # пустая ссылка (маркер декомпилятора)
if ($spec -is [bool]) {
X "$indent$(if ($spec) { 'true' } else { 'false' })"; return
}
if ($spec -is [int] -or $spec -is [long] -or $spec -is [double] -or $spec -is [decimal]) {
X "$indent$(Format-FillNum $spec)"; return
}
$r = Resolve-FillValueSpec "$spec" $typeStr
if ($r.Text -eq '' -and $r.XsiType -eq 'xs:string') { X "$indent"; return }
X "$indent$(Esc-XmlText $r.Text)"
}
# --- 5. Attribute shorthand parser ---
function Build-TypeStr {
param($obj)
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" }
if ($t -and -not $t.Contains('(')) {
if ($t -eq "String" -and $obj.length) {
$t = "String($($obj.length))"
} elseif ($t -eq "Number" -and $obj.length) {
$p = if ($obj.precision) { $obj.precision } else { 0 }
$nn = if ($obj.nonneg -or $obj.nonnegative) { ",nonneg" } else { "" }
$t = "Number($($obj.length),$p$nn)"
}
}
return $t
}
function Parse-AttributeShorthand {
param($val)
if ($val -is [string]) {
$str = "$val"
$parsed = @{
name = ""
type = ""
typeEmpty = $false
synonym = ""
comment = ""
flags = @()
hasFillValue = $false
fillValue = $null
}
# Split by | for flags
$parts = $str -split '\|', 2
$mainPart = $parts[0].Trim()
if ($parts.Count -gt 1) {
$flagStr = $parts[1].Trim()
$parsed.flags = @($flagStr -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
}
# Split by : for name and type
$colonParts = $mainPart -split ':', 2
$parsed.name = $colonParts[0].Trim()
if ($colonParts.Count -gt 1) {
$parsed.type = $colonParts[1].Trim()
}
$parsed.synonym = Split-CamelCase $parsed.name
return $parsed
}
# Object form. synonym/tooltip/format/editFormat — сквозной проброс (строка ИЛИ {ru,en}), НЕ стрингифаим.
# fillCheck — синоним fillChecking (из формы; bool true→ShowError). quickChoice — прощаем bool (true→Use, false→DontUse).
$name = "$($val.name)"
$fc = if ($val.fillChecking) { "$($val.fillChecking)" }
elseif ($null -ne $val.fillCheck) { if ($val.fillCheck -is [bool]) { if ($val.fillCheck) { 'ShowError' } else { '' } } else { "$($val.fillCheck)" } }
else { "" }
$qc = if ($null -ne $val.quickChoice) { if ($val.quickChoice -is [bool]) { if ($val.quickChoice) { 'Use' } else { 'DontUse' } } else { "$($val.quickChoice)" } } else { "" }
return @{
name = $name
type = Build-TypeStr $val
# Явный `type: ""` (ключ присутствует, значение пустое) ≠ отсутствие типа: означает пустой
# (реквизит без типа / произвольный). Отличаем present-"" от absent через $null-проверку (PSCustomObject).
typeEmpty = ($null -ne $val.type -and "$($val.type)".Trim() -eq '' -and -not $val.valueType)
synonym = if ($null -ne $val.synonym) { $val.synonym } else { Split-CamelCase $name }
tooltip = $val.tooltip
comment = if ($val.comment) { "$($val.comment)" } else { "" }
flags = @(if ($val.flags) { $val.flags } else { @() })
fillChecking = $fc
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" }
fullTextSearch = if ($val.fullTextSearch) { "$($val.fullTextSearch)" } else { "" }
fillFromFillingValue = if ($val.fillFromFillingValue -eq $true) { $true } else { $false }
createOnInput = if ($val.createOnInput) { "$($val.createOnInput)" } else { "" }
quickChoice = $qc
dataHistory = if ($val.dataHistory) { "$($val.dataHistory)" } else { "" }
use = if ($val.use) { "$($val.use)" } else { "" }
passwordMode = if ($val.passwordMode -eq $true) { $true } else { $false }
format = $val.format
editFormat = $val.editFormat
mask = if ($val.mask) { "$($val.mask)" } else { "" }
extendedEdit = if ($val.extendedEdit -eq $true) { $true } else { $false }
markNegatives = if ($val.markNegatives -eq $true) { $true } else { $false }
choiceForm = if ($val.choiceForm) { "$($val.choiceForm)" } else { "" }
choiceFoldersAndItems = if ($val.choiceFoldersAndItems) { "$($val.choiceFoldersAndItems)" } else { "" }
minValue = $val.minValue
maxValue = $val.maxValue
hasFillValue = ($val.PSObject -and $val.PSObject.Properties -and ($val.PSObject.Properties.Name -contains 'fillValue'))
fillValue = $val.fillValue
linkByType = $val.linkByType
choiceParameterLinks = $val.choiceParameterLinks
choiceParameters = $val.choiceParameters
master = if ($val.master -eq $true) { $true } else { $false }
mainFilter = if ($val.mainFilter -eq $true) { $true } else { $false }
denyIncompleteValues = if ($val.denyIncompleteValues -eq $true) { $true } else { $false }
useInTotals = if ($null -ne $val.useInTotals) { ($val.useInTotals -eq $true) } else { $true }
baseDimension = if ($val.baseDimension -eq $true) { $true } else { $false }
scheduleLink = $val.scheduleLink
balance = if ($val.balance -eq $true) { $true } else { $false }
accountingFlag = $val.accountingFlag
extDimensionAccountingFlag = $val.extDimensionAccountingFlag
addressingDimension = $val.addressingDimension
}
}
function Parse-EnumValueShorthand {
param($val)
if ($val -is [string]) {
$name = "$val"
return @{
name = $name
synonym = Split-CamelCase $name
comment = ""
}
}
$name = "$($val.name)"
return @{
name = $name
synonym = if ($null -ne $val.synonym) { $val.synonym } else { Split-CamelCase $name } # строка ИЛИ {ru,en} → Emit-MLText
comment = if ($val.comment) { "$($val.comment)" } else { "" }
}
}
# --- 6. GeneratedType categories ---
$script:generatedTypes = @{
"Catalog" = @(
@{ prefix = "CatalogObject"; category = "Object" }
@{ prefix = "CatalogRef"; category = "Ref" }
@{ prefix = "CatalogSelection"; category = "Selection" }
@{ prefix = "CatalogList"; category = "List" }
@{ prefix = "CatalogManager"; category = "Manager" }
)
"Document" = @(
@{ prefix = "DocumentObject"; category = "Object" }
@{ prefix = "DocumentRef"; category = "Ref" }
@{ prefix = "DocumentSelection"; category = "Selection" }
@{ prefix = "DocumentList"; category = "List" }
@{ prefix = "DocumentManager"; category = "Manager" }
)
"Enum" = @(
@{ prefix = "EnumRef"; category = "Ref" }
@{ prefix = "EnumManager"; category = "Manager" }
@{ prefix = "EnumList"; category = "List" }
)
"Constant" = @(
@{ prefix = "ConstantManager"; category = "Manager" }
@{ prefix = "ConstantValueManager"; category = "ValueManager" }
@{ prefix = "ConstantValueKey"; category = "ValueKey" }
)
"InformationRegister" = @(
@{ prefix = "InformationRegisterRecord"; category = "Record" }
@{ prefix = "InformationRegisterManager"; category = "Manager" }
@{ prefix = "InformationRegisterSelection"; category = "Selection" }
@{ prefix = "InformationRegisterList"; category = "List" }
@{ prefix = "InformationRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "InformationRegisterRecordKey"; category = "RecordKey" }
@{ prefix = "InformationRegisterRecordManager"; category = "RecordManager" }
)
"AccumulationRegister" = @(
@{ prefix = "AccumulationRegisterRecord"; category = "Record" }
@{ prefix = "AccumulationRegisterManager"; category = "Manager" }
@{ prefix = "AccumulationRegisterSelection"; category = "Selection" }
@{ prefix = "AccumulationRegisterList"; category = "List" }
@{ prefix = "AccumulationRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
)
"AccountingRegister" = @(
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
@{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" }
@{ prefix = "AccountingRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "AccountingRegisterRecordKey"; category = "RecordKey" }
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
@{ prefix = "AccountingRegisterList"; category = "List" }
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
)
"CalculationRegister" = @(
@{ prefix = "CalculationRegisterRecord"; category = "Record" }
@{ prefix = "CalculationRegisterManager"; category = "Manager" }
@{ prefix = "CalculationRegisterSelection"; category = "Selection" }
@{ prefix = "CalculationRegisterList"; category = "List" }
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
)
"ChartOfAccounts" = @(
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
@{ prefix = "ChartOfAccountsRef"; category = "Ref" }
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
@{ prefix = "ChartOfAccountsList"; category = "List" }
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
)
"ChartOfCharacteristicTypes" = @(
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
@{ prefix = "Characteristic"; category = "Characteristic" }
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
)
"ChartOfCalculationTypes" = @(
@{ prefix = "ChartOfCalculationTypesObject"; category = "Object" }
@{ prefix = "ChartOfCalculationTypesRef"; category = "Ref" }
@{ prefix = "ChartOfCalculationTypesSelection"; category = "Selection" }
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
@{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" }
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
@{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" }
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
@{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" }
)
"BusinessProcess" = @(
@{ prefix = "BusinessProcessObject"; category = "Object" }
@{ prefix = "BusinessProcessRef"; category = "Ref" }
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
@{ prefix = "BusinessProcessList"; category = "List" }
@{ prefix = "BusinessProcessManager"; category = "Manager" }
@{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" }
)
"Task" = @(
@{ prefix = "TaskObject"; category = "Object" }
@{ prefix = "TaskRef"; category = "Ref" }
@{ prefix = "TaskSelection"; category = "Selection" }
@{ prefix = "TaskList"; category = "List" }
@{ prefix = "TaskManager"; category = "Manager" }
)
"ExchangePlan" = @(
@{ prefix = "ExchangePlanObject"; category = "Object" }
@{ prefix = "ExchangePlanRef"; category = "Ref" }
@{ prefix = "ExchangePlanSelection"; category = "Selection" }
@{ prefix = "ExchangePlanList"; category = "List" }
@{ prefix = "ExchangePlanManager"; category = "Manager" }
)
"DefinedType" = @(
@{ prefix = "DefinedType"; category = "DefinedType" }
)
"DocumentJournal" = @(
@{ prefix = "DocumentJournalSelection"; category = "Selection" }
@{ prefix = "DocumentJournalList"; category = "List" }
@{ prefix = "DocumentJournalManager"; category = "Manager" }
)
"Report" = @(
@{ prefix = "ReportObject"; category = "Object" }
@{ prefix = "ReportManager"; category = "Manager" }
)
"DataProcessor" = @(
@{ prefix = "DataProcessorObject"; category = "Object" }
@{ prefix = "DataProcessorManager"; category = "Manager" }
)
"Sequence" = @(
@{ prefix = "SequenceRecord"; category = "Record" }
@{ prefix = "SequenceManager"; category = "Manager" }
@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
)
"FilterCriterion" = @(
@{ prefix = "FilterCriterionManager"; category = "Manager" }
@{ prefix = "FilterCriterionList"; category = "List" }
)
"SettingsStorage" = @(
@{ prefix = "SettingsStorageManager"; category = "Manager" }
)
"WSReference" = @(
@{ prefix = "WSReferenceManager"; category = "Manager" }
)
}
function Emit-InternalInfo {
param([string]$indent, [string]$objectType, [string]$objectName)
$types = $script:generatedTypes[$objectType]
if (-not $types) { return }
X "$indent"
# ExchangePlan: ThisNode UUID before GeneratedTypes
if ($objectType -eq "ExchangePlan") {
X "$indent`t$(New-Guid-String)"
}
foreach ($gt in $types) {
$fullName = "$($gt.prefix).$objectName"
X "$indent`t"
X "$indent`t`t$(New-Guid-String)"
X "$indent`t`t$(New-Guid-String)"
X "$indent`t"
}
X "$indent"
}
# --- 7. StandardAttributes ---
$script:standardAttributesByType = @{
"Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code")
"Document" = @("Posted","Ref","DeletionMark","Date","Number")
"Enum" = @("Order","Ref")
"InformationRegister" = @("Active","LineNumber","Recorder","Period")
"AccumulationRegister" = @("Active","LineNumber","Recorder","Period")
"AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account")
"CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry")
"ChartOfAccounts" = @("PredefinedDataName","Order","OffBalance","Type","Description","Code","Parent","Predefined","DeletionMark","Ref")
"ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType")
"ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","ActionPeriodIsBasic","Description","Code")
"BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask")
"Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess")
"ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo")
"DocumentJournal" = @("Type","Ref","Date","Posted","DeletionMark","Number")
}
# Профиль материализованного блока StandardAttributes (значения, которые платформа заполняет
# автоматически при материализации блока, независимо от структуры каталога). Выведено из корпуса
# (acc+erp: Owner.FFV=true 1592/1596, Owner.FC=ShowError 1589, Parent.FFV=true 1593, Description.FC=ShowError 1467)
# и подтверждено синтетикой. Пока только Catalog (у прочих типов свои профили — добавим при их пилоте).
$script:stdAttrProfile = @{
"Catalog" = @{
"Owner" = @{ FillChecking = "ShowError"; FillFromFillingValue = "true" }
"Parent" = @{ FillFromFillingValue = "true" }
"Description" = @{ FillChecking = "ShowError" }
}
# ExchangePlan: Наименование/Код → FillChecking=ShowError (корпус 40/38 из 41).
"ExchangePlan" = @{
"Description" = @{ FillChecking = "ShowError" }
"Code" = @{ FillChecking = "ShowError" }
}
# ChartOfCharacteristicTypes: Наименование → FillChecking=ShowError (21/23), Родитель → FFV=true (23/23).
"ChartOfCharacteristicTypes" = @{
"Description" = @{ FillChecking = "ShowError" }
"Parent" = @{ FillFromFillingValue = "true" }
}
# ChartOfAccounts: Наименование/Код → FillChecking=ShowError (3/3), Родитель → FFV=true (3/3). Тип (АктивПассив)
# и FillValue Родителя (self EmptyRef) кастомизируются пообъектно → захват override, не профиль.
"ChartOfAccounts" = @{
"Description" = @{ FillChecking = "ShowError" }
"Code" = @{ FillChecking = "ShowError" }
"Parent" = @{ FillFromFillingValue = "true" }
}
# ChartOfCalculationTypes: Наименование → FillChecking=ShowError (Код здесь DontCheck).
"ChartOfCalculationTypes" = @{
"Description" = @{ FillChecking = "ShowError" }
}
# Document: Дата → FillChecking=ShowError (974/1010 доков acc+erp; дата обязательна).
"Document" = @{
"Date" = @{ FillChecking = "ShowError" }
}
}
# $ov — hashtable переопределений (профиль + DSL) для полей: FillChecking, FillFromFillingValue,
# Synonym, FullTextSearch, DataHistory. Прочие поля — фиксированный schema-дефолт.
function Emit-StandardAttribute {
param([string]$indent, [string]$attrName, $ov = $null)
function OvOr { param($k, $d) if ($ov -and $ov.ContainsKey($k)) { return $ov[$k] } else { return $d } }
$fc = OvOr 'FillChecking' 'DontCheck'
$ffv = OvOr 'FillFromFillingValue' 'false'
$dh = OvOr 'DataHistory' 'Use'
$fts = OvOr 'FullTextSearch' 'Use'
$syn = OvOr 'Synonym' ''
$tt = OvOr 'ToolTip' ''
$cf = OvOr 'ChoiceForm' ''
$cmt = OvOr 'Comment' ''
$msk = OvOr 'Mask' ''
$fmt = OvOr 'Format' $null
$efmt = OvOr 'EditFormat' $null
$chi = OvOr 'ChoiceHistoryOnInput' 'Auto'
X "$indent"
# LinkByType стандартного реквизита (напр. ExtDimensionN→Account у регистра бухгалтерии). DataPath verbatim (полный).
$lbt = OvOr 'LinkByType' $null
if ($lbt) {
$lbtDp = if ($lbt.dataPath) { "$($lbt.dataPath)" } else { "$lbt" }
$lbtLi = if ($null -ne $lbt.linkItem) { $lbt.linkItem } else { 0 }
X "$indent`t"
X "$indent`t`t$(Esc-Xml $lbtDp)"
X "$indent`t`t$lbtLi"
X "$indent`t"
} else {
X "$indent`t"
}
X "$indent`t$fc"
X "$indent`tfalse"
X "$indent`t$ffv"
X "$indent`tAuto"
X "$indent`t"
Emit-MLText "$indent`t" "xr:ToolTip" $tt
X "$indent`tfalse"
Emit-MLText "$indent`t" "xr:Format" $fmt
if ($cf) { X "$indent`t$(Esc-Xml "$cf")" } else { X "$indent`t" }
X "$indent`tAuto"
X "$indent`t$chi"
Emit-MLText "$indent`t" "xr:EditFormat" $efmt
X "$indent`tfalse"
X "$indent`t$dh"
X "$indent`tfalse"
X "$indent`t"
Emit-MLText "$indent`t" "xr:Synonym" $syn
if ($cmt) { X "$indent`t$(Esc-XmlText "$cmt")" } else { X "$indent`t" }
X "$indent`t$fts"
Emit-ChoiceParameterLinks "$indent`t" (OvOr 'ChoiceParameterLinks' $null) 'xr:ChoiceParameterLinks'
# FillValue: дефолт nil; override-значение → типизированное (Normalize-ChoiceValue: DTR-путь/строка/bool).
$fvRaw = OvOr 'FillValue' $null
if ($null -eq $fvRaw) { X "$indent`t" }
elseif ($fvRaw.emptyRef -eq $true) { X "$indent`t" }
elseif ($fvRaw.typeDescription -eq $true) { X "$indent`t" } # пустое типизированное (ValueType ПВХ)
else {
$fvN = Normalize-ChoiceValue $fvRaw
if ([string]::IsNullOrEmpty($fvN.Text)) { X "$indent`t" }
else { X "$indent`t$(Esc-Xml $fvN.Text)" }
}
if ($msk) { X "$indent`t$(Esc-XmlText "$msk")" } else { X "$indent`t" }
Emit-ChoiceParameters "$indent`t" (OvOr 'ChoiceParameters' $null) 'xr:ChoiceParameters'
X "$indent"
}
# Единый эмиттер блока StandardAttributes — поведение правят ДАННЫЕ, не форк кода:
# - stdAttrConditionalTypes: типы, где блок материализуется платформой ТОЛЬКО при кастомизации
# ≥1 стандартного реквизита → в DSL это наличие ключа `standardAttributes`. Нет ключа → блок опущен.
# Прочие типы (не в множестве) → блок эмитится всегда (текущее поведение, пока их правило не выведено).
# - stdAttrProfile[тип]: профиль материализованного блока (пусто = schema-дефолт), поверх — DSL-override.
# Миграция типа = добавить его в stdAttrConditionalTypes + stdAttrProfile и переснять снэпшоты; КОД НЕ ТРОГАЕМ.
$script:stdAttrConditionalTypes = @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document')
function Emit-StandardAttributes {
param([string]$indent, [string]$objectType)
$attrs = $script:standardAttributesByType[$objectType]
if (-not $attrs) { return }
$conditional = $script:stdAttrConditionalTypes -contains $objectType
$sa = $def.standardAttributes
if ($conditional -and $null -eq $sa) { return } # условный тип без кастомизации → блока нет
if ($sa -is [string] -and $sa -eq '') { return } # opt-out `standardAttributes:""` (дом-конвенция суппресса, ~5% регистров опускают all-default блок — правило не выводимо)
$profile = $script:stdAttrProfile[$objectType]; if (-not $profile) { $profile = @{} }
# Доп. (опциональные) стандартные реквизиты вне фикс-списка типа — напр. ExchangeDate у части ПланОбмена
# (легаси, присутствие не выводится из свойств). Эмитим по факту наличия ключа в DSL, ПЕРЕД фикс-списком (их позиция).
$extra = @()
if ($sa) { foreach ($k in $sa.PSObject.Properties.Name) { if ($attrs -notcontains $k) { $extra += $k } } }
X "$indent"
foreach ($a in ($extra + $attrs)) {
$ov = @{}
if ($profile.ContainsKey($a)) { foreach ($k in $profile[$a].Keys) { $ov[$k] = $profile[$a][$k] } }
if ($sa) { # DSL-override применяем всегда при наличии ключа (для не-условных типов тоже, напр. ExchangePlan)
$d = $sa.$a
if ($d) {
if ($null -ne $d.synonym) { $ov['Synonym'] = $d.synonym } # строка ИЛИ {ru,en}
if ($null -ne $d.tooltip) { $ov['ToolTip'] = $d.tooltip } # строка ИЛИ {ru,en}
if ($d.fillChecking) { $ov['FillChecking'] = "$($d.fillChecking)" }
if ($null -ne $d.fillFromFillingValue) { $ov['FillFromFillingValue'] = if ($d.fillFromFillingValue) { 'true' } else { 'false' } }
if ($d.fullTextSearch) { $ov['FullTextSearch'] = "$($d.fullTextSearch)" }
if ($d.dataHistory) { $ov['DataHistory'] = "$($d.dataHistory)" }
if ($null -ne $d.fillValue) { $ov['FillValue'] = $d.fillValue } # DTR-путь/строка/bool
if ($null -ne $d.choiceParameterLinks) { $ov['ChoiceParameterLinks'] = $d.choiceParameterLinks }
if ($null -ne $d.choiceParameters) { $ov['ChoiceParameters'] = $d.choiceParameters }
if ($d.comment) { $ov['Comment'] = "$($d.comment)" }
if ($d.mask) { $ov['Mask'] = "$($d.mask)" }
if ($null -ne $d.format) { $ov['Format'] = $d.format } # строка ИЛИ {ru,en}
if ($null -ne $d.editFormat) { $ov['EditFormat'] = $d.editFormat }
if ($d.choiceForm) { $ov['ChoiceForm'] = "$($d.choiceForm)" }
if ($null -ne $d.linkByType) { $ov['LinkByType'] = $d.linkByType }
}
}
Emit-StandardAttribute "$indent`t" $a $ov
}
X "$indent"
}
# TabularSection standard attributes (единственный — LineNumber/НомерСтроки). Блок эмитится всегда (платформа
# опускает его лишь у редкого хвоста ТЧ — правило не выведено, см. WORKFLOW). DSL `lineNumber` на объектной форме ТЧ
# переопределяет свойства (synonym/comment/fullTextSearch/tooltip/format/editFormat/choiceHistoryOnInput).
function Emit-TabularStandardAttributes {
param([string]$indent, $lineNumber = $null)
$ov = $null
if ($lineNumber) {
$ov = @{}
if ($null -ne $lineNumber.synonym) { $ov['Synonym'] = $lineNumber.synonym }
if ($lineNumber.comment) { $ov['Comment'] = "$($lineNumber.comment)" }
if ($lineNumber.fullTextSearch) { $ov['FullTextSearch'] = "$($lineNumber.fullTextSearch)" }
if ($null -ne $lineNumber.tooltip) { $ov['ToolTip'] = $lineNumber.tooltip }
if ($null -ne $lineNumber.format) { $ov['Format'] = $lineNumber.format }
if ($null -ne $lineNumber.editFormat) { $ov['EditFormat'] = $lineNumber.editFormat }
if ($lineNumber.choiceHistoryOnInput) { $ov['ChoiceHistoryOnInput'] = "$($lineNumber.choiceHistoryOnInput)" }
if ($null -ne $lineNumber.fillValue) { $ov['FillValue'] = $lineNumber.fillValue }
}
X "$indent"
Emit-StandardAttribute "$indent`t" "LineNumber" $ov
X "$indent"
}
# --- 8. Attribute emitter ---
$script:reservedAttrNames = @{
"Ref"="Ссылка"; "DeletionMark"="ПометкаУдаления"; "Code"="Код"; "Description"="Наименование"
"Date"="Дата"; "Number"="Номер"; "Posted"="Проведен"; "Parent"="Родитель"; "Owner"="Владелец"
"IsFolder"="ЭтоГруппа"; "Predefined"="Предопределенный"; "PredefinedDataName"="ИмяПредопределенныхДанных"
"Recorder"="Регистратор"; "Period"="Период"; "LineNumber"="НомерСтроки"; "Active"="Активность"
"Order"="Порядок"; "Type"="Тип"; "OffBalance"="Забалансовый"
"Started"="Стартован"; "Completed"="Завершен"; "HeadTask"="ВедущаяЗадача"
"Executed"="Выполнена"; "RoutePoint"="ТочкаМаршрута"; "BusinessProcess"="БизнесПроцесс"
"ThisNode"="ЭтотУзел"; "SentNo"="НомерОтправленного"; "ReceivedNo"="НомерПринятого"
"CalculationType"="ВидРасчета"; "RegistrationPeriod"="ПериодРегистрации"; "ReversingEntry"="СторноЗапись"
"Account"="Счет"; "ValueType"="ТипЗначения"; "ActionPeriodIsBasic"="ПериодДействияБазовый"
}
# Стандартные реквизиты по типу объекта (ключи из reservedAttrNames). Имя реквизита, совпадающее
# с ними (англ. ИЛИ рус.), платформа не позволит — жёсткий отказ. Контексты вне карты → мягкое предупреждение.
$script:reservedByContext = @{
"catalog" = @("Ref","DeletionMark","Predefined","PredefinedDataName","Code","Description","Owner","Parent","IsFolder")
"document" = @("Ref","DeletionMark","Date","Number","Posted")
}
# Стандартный реквизит текущего типа по имени (EN/RU) → EN-имя, либо $null (обычный/неизвестный).
function Resolve-StdAttrEn {
param([string]$name)
$ctx = switch ("$objType") { 'Catalog' { 'catalog' } 'Document' { 'document' } default { $null } }
if (-not $ctx) { return $null }
$stdSet = $script:reservedByContext[$ctx]
foreach ($en in $stdSet) {
$ru = $script:reservedAttrNames[$en]
if (($name -ieq $en) -or ($ru -and $name -ieq $ru)) { return $en }
}
return $null
}
# Прощающий ввод пути к реквизиту САМОГО объекта (dataPath в linkByType/choiceParameterLinks):
# "Ссылка"/"Ref"/станд. → <Тип>.<Имя>.StandardAttribute.; обычное имя → <Тип>.<Имя>.Attribute.<Имя>;
# частичное "StandardAttribute.X"/"Attribute.X" → префикс <Тип>.<Имя>; полный путь → verbatim.
function Expand-DataPath {
param([string]$dp)
if (-not $dp) { return $dp }
$s = "$dp"
if ($s -match '[:/]') { return $s } # спец-путь (напр. 0:GUID/0:GUID в зависимостях ПВХ) — не разворачиваем
if ($s -match '^-?\d+$') { return $s } # голый (отрицательный) индекс-маркер (напр. -8 в ChoiceParameterLinks) — verbatim, не имя реквизита
if ($s -match '^(StandardAttribute|Attribute)\.') { return "$objType.$objName.$s" }
if (-not $s.Contains('.')) {
$en = Resolve-StdAttrEn $s
if ($en) { return "$objType.$objName.StandardAttribute.$en" }
return "$objType.$objName.Attribute.$s"
}
return $s
}
# (связь по типу — тип значения реквизита-Характеристики определяется другим реквизитом).
# Структура как формы: DataPath + LinkItem. DSL `linkByType`: {dataPath, linkItem?} ИЛИ строка-путь.
# Нет ключа → (пусто).
function Emit-LinkByType {
param([string]$indent, $spec)
if (-not $spec) { X "$indent"; return }
if ($spec -is [string]) { $dp = "$spec"; $li = 0 }
else {
$dp = if ($spec.dataPath) { "$($spec.dataPath)" } elseif ($spec.path) { "$($spec.path)" } elseif ($spec.путь) { "$($spec.путь)" } else { "" }
$li = if ($null -ne $spec.linkItem) { $spec.linkItem } elseif ($null -ne $spec.элементСвязи) { $spec.элементСвязи } else { 0 }
}
if (-not $dp) { X "$indent"; return }
$dp = Expand-DataPath $dp
X "$indent"
X "$indent`t$(Esc-Xml "$dp")"
X "$indent`t$li"
X "$indent"
}
# Есть ли ключ в $def (отличаем отсутствие от пустого массива [] = явно пусто).
function Test-DefKey { param([string]$name) return ($def.PSObject -and $def.PSObject.Properties -and ($def.PSObject.Properties.Name -contains $name)) }
# со списком (InputByString/DataLockFields). $fields — готовые полные пути. Пусто → self-close.
function Emit-FieldBlock {
param([string]$indent, [string]$tag, $fields)
$arr = @($fields | Where-Object { "$_" -ne '' })
if ($arr.Count -eq 0) { X "$indent<$tag/>"; return }
X "$indent<$tag>"
foreach ($f in $arr) { X "$indent`t$(Esc-Xml "$f")" }
X "$indent$tag>"
}
# — «ввод на основании», список MDObjectRef ("Catalog.X"/"Document.Y"). Нет ключа/пусто → self-close.
function Emit-BasedOn {
param([string]$indent, $items)
$arr = @($items | Where-Object { $_ })
if ($arr.Count -eq 0) { X "$indent"; return }
X "$indent"
foreach ($it in $arr) { X "$indent`t$(Esc-Xml "$it")" }
X "$indent"
}
# --- Параметры/связи выбора (порт из form-compile; структура реквизита ⟷ элемента формы совпадает) ---
# Свойство из dict/PSCustomObject по списку синонимов (первый найденный, иначе $null).
function Get-ChElProp {
param($obj, [string[]]$names)
if ($null -eq $obj) { return $null }
foreach ($n in $names) {
if ($obj -is [System.Collections.IDictionary]) { if ($obj.Contains($n)) { return $obj[$n] } }
elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { return $obj.PSObject.Properties[$n].Value }
}
return $null
}
# Строковый литерал shorthand → скаляр: true/false→bool, целое/дробное→число, иначе строка.
function ConvertTo-ChScalar {
param([string]$s)
$t = "$s".Trim()
if ($t -match '^(?i:true|истина)$') { return $true }
if ($t -match '^(?i:false|ложь)$') { return $false }
if ($t -match '^-?\d+$') { return [int]$t }
if ($t -match '^-?\d+\.\d+$') { return [double]::Parse($t, [System.Globalization.CultureInfo]::InvariantCulture) }
return $t
}
# Голое значение (без точки) + тип параметра → полный DTR-путь, либо $null. Принимает EnumRef.X / Enum.X / рус.
function Expand-ChoiceRefValue {
param([string]$value, [string]$typeStr)
if (-not $typeStr) { return $null }
$t = Resolve-TypeStr $typeStr
$root = $null; $tn = $null
if ($t -match '^(\w+Ref)\.(.+)$') { $root = $script:fillRefKindRoot[$Matches[1].ToLower()]; $tn = $Matches[2] }
elseif ($t -match '^([^.]+)\.(.+)$') { $root = $script:fillRefRoots[$Matches[1].ToLower()]; $tn = $Matches[2] }
if (-not $root) { return $null }
if ($script:fillEmptyRefWords -contains "$value".ToLower()) { return "$root.$tn.EmptyRef" }
if ($root -eq 'Enum') { return "Enum.$tn.EnumValue.$value" }
return "$root.$tn.$value"
}
# Значение параметра выбора → @{XsiType; Text}. $typeStr (тип параметра) разворачивает голые ref-имена.
function Normalize-ChoiceValueT {
param($value, [string]$typeStr)
if ($typeStr -and ($value -is [string]) -and (-not "$value".Contains('.'))) {
$ex = Expand-ChoiceRefValue "$value" $typeStr
if ($ex) { return @{ XsiType='xr:DesignTimeRef'; Text=$ex } }
}
return Normalize-ChoiceValue $value
}
# Значение параметра выбора → @{XsiType; Text}. Авто-детект по значению (без типа реквизита).
function Normalize-ChoiceValue {
param($value)
if ($value -is [bool]) { return @{ XsiType='xs:boolean'; Text=$(if ($value) { 'true' } else { 'false' }) } }
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
return @{ XsiType='xs:decimal'; Text=(Format-FillNum $value) }
}
$s = "$value"
if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } }
if ($s -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') { return @{ XsiType='xs:dateTime'; Text=$s } }
$ref = Normalize-FillRef $s
if ($ref) { return @{ XsiType='xr:DesignTimeRef'; Text=$ref } }
if ($script:accountTypeValues -contains $s) { return @{ XsiType='ent:AccountType'; Text=$s } }
return @{ XsiType='xs:string'; Text=$s }
}
# Shorthand "name=value" | "name=v1, v2" → {name, value}. "name=path" для links.
function ConvertFrom-ChParamShorthand {
param([string]$s)
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$name = $s.Substring(0, $eq).Trim(); $rest = $s.Substring($eq + 1)
if ($rest -match ',') {
$vals = @(); foreach ($p in ($rest -split ',')) { $vals += ,(ConvertTo-ChScalar $p) }
return @{ name = $name; value = $vals }
}
return @{ name = $name; value = (ConvertTo-ChScalar $rest) }
}
function ConvertFrom-ChLinkShorthand {
param([string]$s)
$eq = $s.IndexOf('=')
if ($eq -lt 0) { return @{ name = $s.Trim() } }
$o = @{ name = $s.Substring(0, $eq).Trim() }; $rest = $s.Substring($eq + 1).Trim()
if ($rest -match '^(.*):(?i:(Clear|DontChange|очистить|неизменять))$') { $o['dataPath'] = $matches[1].Trim(); $o['valueChange'] = $matches[2] }
else { $o['dataPath'] = $rest }
return $o
}
# — [{name, value?}]. Значение ПРЯМО на app:value (xsi:type=тип); массив → v8:FixedArray
# с детьми v8:Value; без value → app:value nil.
function Emit-ChoiceParameters {
param([string]$indent, $cp, [string]$tag = 'ChoiceParameters')
if (-not $cp -or @($cp).Count -eq 0) { X "$indent<$tag/>"; return }
X "$indent<$tag>"
foreach ($item in @($cp)) {
if ($item -is [string]) { $item = ConvertFrom-ChParamShorthand $item }
$name = Get-ChElProp $item @('name','имя')
$ptype = Get-ChElProp $item @('type','тип')
$hasVal = $false; $val = $null
if ($item -is [System.Collections.IDictionary]) {
if ($item.Contains('value')) { $hasVal = $true; $val = $item['value'] }
elseif ($item.Contains('значение')) { $hasVal = $true; $val = $item['значение'] }
} elseif ($item.PSObject) {
if ($item.PSObject.Properties['value']) { $hasVal = $true; $val = $item.PSObject.Properties['value'].Value }
elseif ($item.PSObject.Properties['значение']) { $hasVal = $true; $val = $item.PSObject.Properties['значение'].Value }
}
$valIsArray = ($val -is [System.Array]) -or ($val -is [System.Collections.IList] -and $val -isnot [string])
X "$indent`t"
if (-not $hasVal) {
X "$indent`t`t"
} elseif ($valIsArray) {
X "$indent`t`t"
foreach ($v in $val) {
$norm = Normalize-ChoiceValueT $v $ptype
if ([string]::IsNullOrEmpty($norm.Text)) { X "$indent`t`t`t" }
else { X "$indent`t`t`t$(Esc-Xml $norm.Text)" }
}
X "$indent`t`t"
} else {
$norm = Normalize-ChoiceValueT $val $ptype
if ([string]::IsNullOrEmpty($norm.Text)) { X "$indent`t`t" }
else { X "$indent`t`t$(Esc-Xml $norm.Text)" }
}
X "$indent`t"
}
X "$indent$tag>"
}
# — [{name, dataPath, valueChange?}]. valueChange дефолт Clear.
function Emit-ChoiceParameterLinks {
param([string]$indent, $cpl, [string]$tag = 'ChoiceParameterLinks')
if (-not $cpl -or @($cpl).Count -eq 0) { X "$indent<$tag/>"; return }
X "$indent<$tag>"
foreach ($lk in @($cpl)) {
if ($lk -is [string]) { $lk = ConvertFrom-ChLinkShorthand $lk }
$name = Get-ChElProp $lk @('name','имя')
$dp = Expand-DataPath (Get-ChElProp $lk @('dataPath','path','путь'))
$vcRaw = Get-ChElProp $lk @('valueChange','режимИзменения')
$vc = 'Clear'
if ($vcRaw) {
$vc = switch -Regex ("$vcRaw".ToLower()) {
'^(clear|очистить|очистка)$' { 'Clear'; break }
'^(dontchange|неизменять|неменять|нет)$' { 'DontChange'; break }
default { "$vcRaw" }
}
}
X "$indent`t"
X "$indent`t`t$(Esc-Xml "$name")"
X "$indent`t`t$(Esc-Xml "$dp")"
X "$indent`t`t$vc"
X "$indent`t"
}
X "$indent$tag>"
}
# --- Characteristics (привязка ПВХ «Дополнительные реквизиты и сведения») ---
# from: рус. корень (Справочник→Catalog) + член (ТабличнаяЧасть→TabularSection); короткая 3-сегментная
# "<Тип>.X.Y" → вставить TabularSection (from — всегда таблица, не реквизит). Полный путь → как есть.
function Normalize-CharFrom {
param([string]$from)
if (-not $from) { return $from }
$parts = @("$from" -split '\.')
if ($script:objectTypeSynonyms.ContainsKey($parts[0])) { $parts[0] = $script:objectTypeSynonyms[$parts[0]] }
for ($i = 1; $i -lt $parts.Count; $i++) {
switch -Regex ($parts[$i]) {
'^ТабличнаяЧасть$' { $parts[$i] = 'TabularSection' }
'^Измерение$' { $parts[$i] = 'Dimension' }
'^Ресурс$' { $parts[$i] = 'Resource' }
'^Реквизит$' { $parts[$i] = 'Attribute' }
}
}
if ($parts.Count -eq 3 -and $parts[0] -in @('Catalog','Document','ChartOfCharacteristicTypes','ChartOfCalculationTypes','ChartOfAccounts','ExchangePlan','BusinessProcess','Task')) {
$parts = @($parts[0], $parts[1], 'TabularSection', $parts[2])
}
return ($parts -join '.')
}
# Стандартный реквизит ссылочного типа в полях Characteristics: Ref/Parent/Owner (по имени EN/RU).
# Прочие стандартные реквизиты редки в полях — их задают частичной формой StandardAttribute.X.
function Resolve-CharStdEn {
param([string]$name)
$n = "$name".ToLower()
if ($n -eq 'ref' -or $n -eq 'ссылка') { return 'Ref' }
if ($n -eq 'parent' -or $n -eq 'родитель') { return 'Parent' }
if ($n -eq 'owner' -or $n -eq 'владелец') { return 'Owner' }
return $null
}
# Поле: голое→StandardAttribute./Attribute.<имя>; частичное Member.X→.Member.X; полный путь→verbatim.
function Expand-CharField {
param([string]$field, [string]$from)
$s = "$field"
if (-not $s) { return $s }
if ($s -eq '-1') { return '-1' } # поле не задано (empty-характеристика) — как есть
if ($s -match '^(StandardAttribute|Attribute|Dimension|Resource)\.') { return "$from.$s" }
if (-not $s.Contains('.')) {
$en = Resolve-CharStdEn $s
if ($en) { return "$from.StandardAttribute.$en" }
return "$from.Attribute.$s"
}
return $s
}
# Числовое поле-флаг Characteristics (DataPathField/MultipleValues*) — дефолт -1.
function Get-CharIntField { param($obj, [string[]]$names) $v = Get-ChElProp $obj $names; if ($null -eq $v -or "$v" -eq '') { return -1 } return [int]$v }
function Emit-Characteristics {
param([string]$indent, $chars)
if (-not $chars -or @($chars).Count -eq 0) { X "$indent"; return }
X "$indent"
foreach ($ch in @($chars)) {
$types = Get-ChElProp $ch @('types','characteristicTypes','типы')
$values = Get-ChElProp $ch @('values','characteristicValues','значения')
$tFrom = Normalize-CharFrom "$(Get-ChElProp $types @('from','source','источник'))"
$vFrom = Normalize-CharFrom "$(Get-ChElProp $values @('from','source','источник'))"
$key = Expand-CharField "$(Get-ChElProp $types @('key','keyField'))" $tFrom
$tff = Expand-CharField "$(Get-ChElProp $types @('filterField','typesFilterField'))" $tFrom
$obj = Expand-CharField "$(Get-ChElProp $values @('object','objectField'))" $vFrom
$typ = Expand-CharField "$(Get-ChElProp $values @('type','typeField'))" $vFrom
$val = Expand-CharField "$(Get-ChElProp $values @('value','valueField'))" $vFrom
# числовые поля-флаги (обычно -1; иногда 0)
$dpf = Get-CharIntField $types @('dataPathField')
$mvu = Get-CharIntField $types @('multipleValuesUseField')
$mvk = Get-CharIntField $values @('multipleValuesKeyField')
$mvo = Get-CharIntField $values @('multipleValuesOrderField')
X "$indent`t"
X "$indent`t`t"
X "$indent`t`t`t$(Esc-Xml $key)"
X "$indent`t`t`t$(Esc-Xml $tff)"
# filterValue: $null→nil; голое→xs:string, полный путь→DTR, bool→xs:boolean.
$tfvRaw = Get-ChElProp $types @('filterValue','typesFilterValue')
if ($null -eq $tfvRaw) { X "$indent`t`t`t" }
else {
$tfvN = Normalize-ChoiceValue $tfvRaw
if ([string]::IsNullOrEmpty($tfvN.Text)) { X "$indent`t`t`t" }
else { X "$indent`t`t`t$(Esc-Xml $tfvN.Text)" }
}
X "$indent`t`t`t$dpf"
X "$indent`t`t`t$mvu"
X "$indent`t`t"
X "$indent`t`t"
X "$indent`t`t`t$(Esc-Xml $obj)"
X "$indent`t`t`t$(Esc-Xml $typ)"
X "$indent`t`t`t$(Esc-Xml $val)"
X "$indent`t`t`t$mvk"
X "$indent`t`t`t$mvo"
X "$indent`t`t"
X "$indent`t"
}
X "$indent"
}
# / — граница диапазона реквизита. Нет ключа → nil (не задано). Значение типизировано
# (зеркало form-compile): число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
function Emit-MinMaxValue {
param([string]$indent, [string]$tag, $val)
if ($null -eq $val) { X "$indent<$tag xsi:nil=`"true`"/>"; return }
$t = if ($val -is [string]) { 'xs:string' } else { 'xs:decimal' }
X "$indent<$tag xsi:type=`"$t`">$(Esc-Xml "$val")$tag>"
}
function Emit-Attribute {
param([string]$indent, $parsed, [string]$context, [string]$elemTag = "Attribute")
# $context: "catalog", "document", "object", "processor", "tabular", "processor-tabular", "register",
# "account" (реквизит Плана счетов: как catalog, но без