# meta-edit v1.19 — Edit existing 1C metadata object XML (+add-predefined предопределённые Ext/Predefined.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, [Parameter(Mandatory)] [Alias('Path')] [string]$ObjectPath, # Inline mode (alternative to DefinitionFile) [ValidateSet( "add-attribute", "add-ts", "add-dimension", "add-resource", "add-enumValue", "add-column", "add-form", "add-template", "add-command", "add-owner", "add-registerRecord", "add-basedOn", "add-inputByString", "add-dataLockField", "add-registeredDocument", "add-predefined", "remove-attribute", "remove-ts", "remove-dimension", "remove-resource", "remove-enumValue", "remove-column", "remove-form", "remove-template", "remove-command", "remove-owner", "remove-registerRecord", "remove-basedOn", "remove-inputByString", "remove-dataLockField", "remove-registeredDocument", "add-ts-attribute", "remove-ts-attribute", "modify-ts-attribute", "modify-ts", "modify-attribute", "modify-dimension", "modify-resource", "modify-enumValue", "modify-column", "modify-property", "set-owners", "set-registerRecords", "set-basedOn", "set-inputByString", "set-dataLockFields", "set-registeredDocuments" )] [string]$Operation, [string]$Value, [switch]$NoValidate ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # ============================================================ # Section 1: Parameters + loading # ============================================================ # --- Mode validation --- if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation" exit 1 } if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required" exit 1 } # --- Enum value normalization (same as meta-compile) --- $script:enumValueAliases = @{ "Balances" = "Balance"; "Остатки" = "Balance"; "Обороты" = "Turnovers" "RecordSubordinate" = "RecorderSubordinate"; "Subordinate" = "RecorderSubordinate" "ПодчинениеРегистратору" = "RecorderSubordinate"; "Независимый" = "Independent" "NotDependOnCalculationTypes" = "DontUse"; "NoDependence" = "DontUse"; "NotUsed" = "DontUse" "Depend" = "OnActionPeriod"; "ПоПериодуДействия" = "OnActionPeriod" "None" = "Nonperiodical"; "Daily" = "Day"; "Monthly" = "Month" "Quarterly" = "Quarter"; "Yearly" = "Year" "Непериодический" = "Nonperiodical"; "Секунда" = "Second"; "День" = "Day"; "Месяц" = "Month" "Квартал" = "Quarter"; "Год" = "Year" "ПозицияРегистратора" = "RecorderPosition" "Автоматический" = "Automatic"; "Управляемый" = "Managed" "Использовать" = "Use"; "НеИспользовать" = "DontUse" "Разрешить" = "Allow"; "Запретить" = "Deny" "ВДиалоге" = "InDialog"; "ВСписке" = "InList"; "ОбаСпособа" = "BothWays" "ВВидеНаименования" = "AsDescription"; "ВВидеКода" = "AsCode" "НеПроверять" = "DontCheck"; "Ошибка" = "ShowError"; "Предупреждение" = "ShowWarning" "НеИндексировать" = "DontIndex"; "Индексировать" = "Index" "ИндексироватьСДопУпорядочиванием" = "IndexWithAdditionalOrder" } $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") } 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 } # --- Load JSON definition (DefinitionFile mode) --- $def = $null if ($DefinitionFile) { if (-not (Test-Path $DefinitionFile)) { Write-Error "Definition file not found: $DefinitionFile" exit 1 } $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile $def = $jsonText | ConvertFrom-Json } # --- Resolve object path --- if (Test-Path $ObjectPath -PathType Container) { $dirName = Split-Path $ObjectPath -Leaf $candidate = Join-Path $ObjectPath "$dirName.xml" $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" if (Test-Path $candidate) { $ObjectPath = $candidate } elseif (Test-Path $sibling) { $ObjectPath = $sibling } else { Write-Error "Directory given but no $dirName.xml found inside or as sibling" exit 1 } } # File not found — check Dir/Name/Name.xml → Dir/Name.xml if (-not (Test-Path $ObjectPath)) { $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) $parentDir = Split-Path $ObjectPath $parentDirName = Split-Path $parentDir -Leaf if ($fileName -eq $parentDirName) { $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" if (Test-Path $candidate) { $ObjectPath = $candidate } } } if (-not (Test-Path $ObjectPath)) { Write-Error "Object file not found: $ObjectPath" exit 1 } $resolvedPath = (Resolve-Path $ObjectPath).Path # --- 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 $resolvedPath 'editable' # --- Load XML --- $script:xmlDoc = New-Object System.Xml.XmlDocument $script:xmlDoc.PreserveWhitespace = $true $script:xmlDoc.Load($resolvedPath) # --- Counters --- $script:addCount = 0 $script:removeCount = 0 $script:modifyCount = 0 $script:warnCount = 0 function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow $script:warnCount++ } function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan } # ============================================================ # Section 2: Detect object type # ============================================================ $root = $script:xmlDoc.DocumentElement if ($root.LocalName -ne "MetaDataObject") { Write-Error "Root element must be MetaDataObject, got: $($root.LocalName)" exit 1 } # Find the first child element — this is the object type element $script:objElement = $null foreach ($child in $root.ChildNodes) { if ($child.NodeType -eq 'Element') { $script:objElement = $child break } } if (-not $script:objElement) { Write-Error "No object element found under MetaDataObject" exit 1 } $script:objType = $script:objElement.LocalName $script:mdNs = $script:objElement.NamespaceURI # Find Properties and ChildObjects $script:propertiesEl = $null $script:childObjectsEl = $null foreach ($child in $script:objElement.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } if ($child.LocalName -eq "Properties") { $script:propertiesEl = $child } if ($child.LocalName -eq "ChildObjects") { $script:childObjectsEl = $child } } if (-not $script:propertiesEl) { Write-Error "No found in $($script:objType)" exit 1 } # Extract object name $script:objName = "" foreach ($child in $script:propertiesEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { $script:objName = $child.InnerText.Trim() break } } Info "Object: $($script:objType).$($script:objName)" # ============================================================ # Section 3: Synonym tables # ============================================================ # Operation synonyms $script:operationSynonyms = @{ "add" = "add"; "добавить" = "add" "remove" = "remove"; "удалить" = "remove" "modify" = "modify"; "изменить" = "modify" } # Child type synonyms $script:childTypeSynonyms = @{ "attributes" = "attributes"; "реквизиты" = "attributes"; "attrs" = "attributes" "tabularsections" = "tabularSections"; "табличныечасти" = "tabularSections"; "тч" = "tabularSections"; "ts" = "tabularSections" "dimensions" = "dimensions"; "измерения" = "dimensions"; "dims" = "dimensions" "resources" = "resources"; "ресурсы" = "resources"; "res" = "resources" "enumvalues" = "enumValues"; "значения" = "enumValues"; "values" = "enumValues" "columns" = "columns"; "графы" = "columns"; "колонки" = "columns" "forms" = "forms"; "формы" = "forms" "templates" = "templates"; "макеты" = "templates" "commands" = "commands"; "команды" = "commands" "properties" = "properties"; "свойства" = "properties" } # Type synonyms (from meta-compile) $script:typeSynonyms = New-Object System.Collections.Hashtable $script:typeSynonyms["число"] = "Number" $script:typeSynonyms["строка"] = "String" $script:typeSynonyms["булево"] = "Boolean" $script:typeSynonyms["дата"] = "Date" $script:typeSynonyms["датавремя"]= "DateTime" $script:typeSynonyms["хранилищезначения"] = "ValueStorage" $script:typeSynonyms["number"] = "Number" $script:typeSynonyms["string"] = "String" $script:typeSynonyms["boolean"] = "Boolean" $script:typeSynonyms["date"] = "Date" $script:typeSynonyms["datetime"] = "DateTime" $script:typeSynonyms["valuestorage"] = "ValueStorage" $script:typeSynonyms["bool"] = "Boolean" # Reference synonyms $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" $script:typeSynonyms["catalogref"] = "CatalogRef" $script:typeSynonyms["documentref"] = "DocumentRef" $script:typeSynonyms["enumref"] = "EnumRef" # ============================================================ # Section 4: Type system # ============================================================ function Esc-Xml { param([string]$s) return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') } function Split-CamelCase { param([string]$name) if (-not $name) { return $name } $result = [regex]::Replace($name, '([а-яё])([А-ЯЁ])', '$1 $2') $result = [regex]::Replace($result, '([a-z])([A-Z])', '$1 $2') if ($result.Length -gt 1) { $result = $result.Substring(0,1) + $result.Substring(1).ToLower() } return $result } function New-Guid-String { return [System.Guid]::NewGuid().ToString() } function Resolve-TypeStr { param([string]$typeStr) if (-not $typeStr) { return $typeStr } # Parameterized: Number(15,2), Строка(100) if ($typeStr -match '^([^(]+)\((.+)\)$') { $baseName = $Matches[1].Trim() $params = $Matches[2] $resolved = $script:typeSynonyms[$baseName.ToLower()] if ($resolved) { return "$resolved($params)" } return $typeStr } # Reference: СправочникСсылка.Организации if ($typeStr.Contains('.')) { $dotIdx = $typeStr.IndexOf('.') $prefix = $typeStr.Substring(0, $dotIdx) $suffix = $typeStr.Substring($dotIdx) $resolved = $script:typeSynonyms[$prefix.ToLower()] if ($resolved) { return "$resolved$suffix" } return $typeStr } # Simple $resolved = $script:typeSynonyms[$typeStr.ToLower()] if ($resolved) { return $resolved } return $typeStr } function Build-TypeContentXml { param([string]$indent, [string]$typeStr) if (-not $typeStr) { return "" } # Composite type: "Type1 + Type2 + Type3" if ($typeStr.Contains(' + ')) { $parts = $typeStr -split '\s*\+\s*' $sb = New-Object System.Text.StringBuilder foreach ($part in $parts) { $inner = Build-TypeContentXml $indent $part.Trim() if ($inner) { $sb.AppendLine($inner) | Out-Null } } return $sb.ToString().TrimEnd("`r","`n") } $typeStr = Resolve-TypeStr $typeStr $sb = New-Object System.Text.StringBuilder # Boolean if ($typeStr -eq "Boolean") { $sb.AppendLine("$indentxs:boolean") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # ValueStorage if ($typeStr -eq "ValueStorage") { $sb.AppendLine("$indentxs:base64Binary") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # String or String(N) if ($typeStr -match '^String(\((\d+)\))?$') { $len = if ($Matches[2]) { $Matches[2] } else { "10" } $sb.AppendLine("$indentxs:string") | Out-Null $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t$len") | Out-Null $sb.AppendLine("$indent`tVariable") | Out-Null $sb.AppendLine("$indent") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # 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" } $sb.AppendLine("$indentxs:decimal") | Out-Null $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t$digits") | Out-Null $sb.AppendLine("$indent`t$fraction") | Out-Null $sb.AppendLine("$indent`t$sign") | Out-Null $sb.AppendLine("$indent") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # Number without params → Number(10,0) if ($typeStr -eq "Number") { $sb.AppendLine("$indentxs:decimal") | Out-Null $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t10") | Out-Null $sb.AppendLine("$indent`t0") | Out-Null $sb.AppendLine("$indent`tAny") | Out-Null $sb.AppendLine("$indent") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # Date / DateTime if ($typeStr -eq "Date") { $sb.AppendLine("$indentxs:dateTime") | Out-Null $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`tDate") | Out-Null $sb.AppendLine("$indent") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } if ($typeStr -eq "DateTime") { $sb.AppendLine("$indentxs:dateTime") | Out-Null $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`tDateTime") | Out-Null $sb.AppendLine("$indent") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # DefinedType if ($typeStr -match '^DefinedType\.(.+)$') { $dtName = $Matches[1] $sb.AppendLine("$indentcfg:DefinedType.$dtName") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # Reference types — use local xmlns declaration for 1C compatibility if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { $sb.AppendLine("$indentd5p1:$typeStr") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } # Fallback $sb.AppendLine("$indent$typeStr") | Out-Null return $sb.ToString().TrimEnd("`r","`n") } function Build-ValueTypeXml { param([string]$indent, [string]$typeStr) $inner = Build-TypeContentXml "$indent`t" $typeStr return "$indent`r`n$inner`r`n$indent" } function Build-FillValueXml { param([string]$indent, [string]$typeStr) if (-not $typeStr) { return "$indent" } $typeStr = Resolve-TypeStr $typeStr if ($typeStr -eq "Boolean") { return "$indentfalse" } if ($typeStr -match '^String') { return "$indent" } if ($typeStr -match '^Number') { return "$indent0" } return "$indent" } function Build-MLTextXml { param([string]$indent, [string]$tag, [string]$text) if (-not $text) { return "$indent<$tag/>" } $lines = @( "$indent<$tag>" "$indent`t" "$indent`t`tru" "$indent`t`t$(Esc-Xml $text)" "$indent`t" "$indent" ) return $lines -join "`r`n" } # ============================================================ # Section 5: DOM helpers # ============================================================ $script:metaNs = "http://v8.1c.ru/8.3/MDClasses" $script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" $script:v8Ns = "http://v8.1c.ru/8.1/data/core" function Import-Fragment([string]$xmlString) { $wrapper = @" <_W xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:xs="http://www.w3.org/2001/XMLSchema">$xmlString "@ $frag = New-Object System.Xml.XmlDocument $frag.PreserveWhitespace = $true $frag.LoadXml($wrapper) $nodes = @() foreach ($child in $frag.DocumentElement.ChildNodes) { if ($child.NodeType -eq 'Element') { $nodes += $script:xmlDoc.ImportNode($child, $true) } } return ,$nodes } function Get-ChildIndent($container) { foreach ($child in $container.ChildNodes) { if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { $text = $child.Value if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } if ($text -match '^\r?\n(\t+)') { return $Matches[1] } } } # Fallback: count depth $depth = 0 $current = $container while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++ $current = $current.ParentNode } return "`t" * ($depth + 1) } function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") if ($refNode) { $container.InsertBefore($ws, $refNode) | Out-Null $container.InsertBefore($newNode, $ws) | Out-Null } else { $trailing = $container.LastChild if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { $container.InsertBefore($ws, $trailing) | Out-Null $container.InsertBefore($newNode, $trailing) | Out-Null } else { $container.AppendChild($ws) | Out-Null $container.AppendChild($newNode) | Out-Null $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") $container.AppendChild($closeWs) | Out-Null } } } function Remove-NodeWithWhitespace($node) { $parent = $node.ParentNode $prev = $node.PreviousSibling $next = $node.NextSibling if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { $parent.RemoveChild($prev) | Out-Null } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { $parent.RemoveChild($next) | Out-Null } $parent.RemoveChild($node) | Out-Null } function Find-ElementByName($container, [string]$elemLocalName, [string]$nameValue) { foreach ($child in $container.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } if ($child.LocalName -ne $elemLocalName) { continue } # Look for Properties/Name or just Name child $propsEl = $null foreach ($gc in $child.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { $propsEl = $gc; break } } $searchIn = if ($propsEl) { $propsEl } else { $child } foreach ($gc in $searchIn.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name" -and $gc.InnerText.Trim() -eq $nameValue) { return $child } } } return $null } function Find-LastElementOfType($container, [string]$localName) { $last = $null foreach ($child in $container.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { $last = $child } } return $last } function Find-FirstElementOfType($container, [string]$localName) { foreach ($child in $container.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { return $child } } return $null } function Ensure-ChildObjectsOpen { if ($script:childObjectsEl) { # Check if it's self-closing (no child elements) $hasElements = $false foreach ($ch in $script:childObjectsEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } } if (-not $hasElements) { # It's empty — we need to add whitespace for proper formatting $indent = Get-ChildIndent $script:objElement $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") $script:childObjectsEl.AppendChild($closeWs) | Out-Null } return } # No ChildObjects at all — create one after Properties $indent = Get-ChildIndent $script:objElement $coXml = "`r`n$indent`r`n$indent" # Insert after Properties $refNode = $null $foundProps = $false foreach ($child in $script:objElement.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Properties") { $foundProps = $true continue } if ($foundProps -and $child.NodeType -eq 'Element') { $refNode = $child break } } $coEl = $script:xmlDoc.CreateElement("ChildObjects", $script:mdNs) $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") $coEl.AppendChild($closeWs) | Out-Null $wsB = $script:xmlDoc.CreateWhitespace("`r`n$indent") if ($refNode) { $script:objElement.InsertBefore($wsB, $refNode) | Out-Null $script:objElement.InsertBefore($coEl, $wsB) | Out-Null } else { # After last child $trailing = $script:objElement.LastChild if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { $script:objElement.InsertBefore($wsB, $trailing) | Out-Null $script:objElement.InsertBefore($coEl, $trailing) | Out-Null } else { $script:objElement.AppendChild($wsB) | Out-Null $script:objElement.AppendChild($coEl) | Out-Null } } $script:childObjectsEl = $coEl } function Collapse-ChildObjectsIfEmpty { if (-not $script:childObjectsEl) { return } $hasElements = $false foreach ($ch in $script:childObjectsEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } } if (-not $hasElements) { # Remove all whitespace children while ($script:childObjectsEl.HasChildNodes) { $script:childObjectsEl.RemoveChild($script:childObjectsEl.FirstChild) | Out-Null } } } # ============================================================ # Section 6: Fragment builders # ============================================================ function Parse-AttributeShorthand { param($val) if ($val -is [string]) { $str = "$val" $parsed = @{ name = ""; type = ""; synonym = ""; comment = "" flags = @(); fillChecking = ""; indexing = "" after = ""; before = "" } # Extract positional markers: >> after Name, << before Name if ($str -match '\s*>>\s*after\s+(\S+)\s*$') { $parsed.after = $Matches[1] $str = ($str -replace '\s*>>\s*after\s+\S+\s*$', '').Trim() } elseif ($str -match '\s*<<\s*before\s+(\S+)\s*$') { $parsed.before = $Matches[1] $str = ($str -replace '\s*<<\s*before\s+\S+\s*$', '').Trim() } # 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 $name = "$($val.name)" $result = @{ name = $name type = if ($val.type -is [array]) { ($val.type | ForEach-Object { "$_" }) -join ' + ' } elseif ($val.type) { "$($val.type)" } else { "" } synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } comment = if ($val.comment) { "$($val.comment)" } else { "" } flags = @(if ($val.flags) { $val.flags } else { @() }) fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" } indexing = if ($val.indexing) { "$($val.indexing)" } else { "" } after = if ($val.after) { "$($val.after)" } else { "" } before = if ($val.before) { "$($val.before)" } else { "" } } # Map flags to properties if ($result.flags -contains "req" -and -not $result.fillChecking) { $result.fillChecking = "ShowError" } if ($result.flags -contains "index" -and -not $result.indexing) { $result.indexing = "Index" } if ($result.flags -contains "indexadditional" -and -not $result.indexing) { $result.indexing = "IndexWithAdditionalOrder" } return $result } function Parse-EnumValueShorthand { param($val) if ($val -is [string]) { $name = "$val" return @{ name = $name synonym = Split-CamelCase $name comment = "" after = ""; before = "" } } $name = "$($val.name)" return @{ name = $name synonym = if ($val.synonym) { "$($val.synonym)" } else { Split-CamelCase $name } comment = if ($val.comment) { "$($val.comment)" } else { "" } after = if ($val.after) { "$($val.after)" } else { "" } before = if ($val.before) { "$($val.before)" } else { "" } } } # Determine attribute context from object type function Get-AttributeContext { switch ($script:objType) { "Catalog" { return "catalog" } "Document" { return "document" } { $_ -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister") } { return "register" } { $_ -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport") } { return "processor" } default { return "object" } } } $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") } function Build-AttributeFragment { param($parsed, [string]$context, [string]$indent) if (-not $context) { $context = Get-AttributeContext } # Check reserved attribute names (типозависимо: catalog/document — жёсткий отказ; прочее — предупреждение) $attrName = $parsed.name $ctxReserved = $script:reservedByContext[$context] if ($ctxReserved) { foreach ($en in $ctxReserved) { $ru = $script:reservedAttrNames[$en] if (($attrName -ieq $en) -or ($ru -and $attrName -ieq $ru)) { Write-Error "Имя реквизита '$attrName' зарезервировано стандартным реквизитом ($en/$ru) объекта '$context'. Выберите другое имя." exit 1 } } } elseif ($context -notin @("tabular", "processor-tabular") -and ($script:reservedAttrNames.ContainsKey($attrName) -or ($script:reservedAttrNames.Values -contains $attrName))) { Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C." } $uuid = New-Guid-String $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null # Type $typeStr = $parsed.type if ($typeStr) { $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null } else { $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t`txs:string") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null # FillFromFillingValue/FillValue — not for register, tabular (config TS), or processor (non-stored top-level) if ($context -notin @("register", "tabular", "processor")) { $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine($(Build-FillValueXml "$indent`t`t" $typeStr)) | Out-Null } # FillChecking $fillChecking = "DontCheck" if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } if ($parsed.fillChecking) { $fillChecking = Normalize-EnumValue "FillChecking" $parsed.fillChecking } $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null $sb.AppendLine("$indent`t`tItems") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null # Use — catalog only if ($context -eq "catalog") { $sb.AppendLine("$indent`t`tForItem") | Out-Null } # Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular) if ($context -notin @("processor", "processor-tabular")) { $indexing = "DontIndex" if ($parsed.flags -contains "index") { $indexing = "Index" } if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" } if ($parsed.indexing) { $indexing = Normalize-EnumValue "Indexing" $parsed.indexing } $sb.AppendLine("$indent`t`t$indexing") | Out-Null $sb.AppendLine("$indent`t`tUse") | Out-Null $sb.AppendLine("$indent`t`tUse") | Out-Null } $sb.AppendLine("$indent`t") | Out-Null $sb.Append("$indent") | Out-Null return $sb.ToString() } function Build-TabularSectionFragment { param($tsDef, [string]$indent) $tsName = "$($tsDef.name)" $tsSynonym = if ($tsDef.synonym) { "$($tsDef.synonym)" } else { Split-CamelCase $tsName } $uuid = New-Guid-String $objType = $script:objType $objName = $script:objName $typePrefix = "${objType}TabularSection" $rowPrefix = "${objType}TabularSectionRow" $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent") | Out-Null # InternalInfo $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null $sb.AppendLine("$indent`t`t`t$(New-Guid-String)") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t") | Out-Null # Properties $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $tsName)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $tsSynonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tDontCheck") | Out-Null # StandardAttributes (LineNumber) $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`tDontCheck") | Out-Null $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t`t`tUse") | Out-Null $sb.AppendLine("$indent`t`t`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`tUse") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null # Use — catalog only if ($objType -eq "Catalog") { $sb.AppendLine("$indent`t`tForItem") | Out-Null } $sb.AppendLine("$indent`t") | Out-Null # ChildObjects with attrs $columns = @() if ($tsDef.attrs) { $columns = @($tsDef.attrs) } elseif ($tsDef.attributes) { $columns = @($tsDef.attributes) } elseif ($tsDef.реквизиты) { $columns = @($tsDef.реквизиты) } $tsAttrContext = if ($script:objType -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport")) { "processor-tabular" } else { "tabular" } if ($columns.Count -gt 0) { $sb.AppendLine("$indent`t") | Out-Null foreach ($col in $columns) { $colParsed = Parse-AttributeShorthand $col $sb.AppendLine($(Build-AttributeFragment $colParsed $tsAttrContext "$indent`t`t")) | Out-Null } $sb.AppendLine("$indent`t") | Out-Null } else { $sb.AppendLine("$indent`t") | Out-Null } $sb.Append("$indent") | Out-Null return $sb.ToString() } function Build-DimensionFragment { param($parsed, [string]$registerType, [string]$indent) if (-not $registerType) { $registerType = $script:objType } $uuid = New-Guid-String $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $typeStr = $parsed.type if ($typeStr) { $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null } else { $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t`txs:string") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null # InformationRegister: FillFromFillingValue, FillValue if ($registerType -eq "InformationRegister") { $fillFrom = if ($parsed.flags -contains "master") { "true" } else { "false" } $sb.AppendLine("$indent`t`t$fillFrom") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } $fillChecking = "DontCheck" if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null $sb.AppendLine("$indent`t`tItems") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null # InformationRegister: Master, MainFilter, DenyIncompleteValues if ($registerType -eq "InformationRegister") { $master = if ($parsed.flags -contains "master") { "true" } else { "false" } $mainFilter = if ($parsed.flags -contains "mainfilter") { "true" } else { "false" } $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } $sb.AppendLine("$indent`t`t$master") | Out-Null $sb.AppendLine("$indent`t`t$mainFilter") | Out-Null $sb.AppendLine("$indent`t`t$denyIncomplete") | Out-Null } # AccumulationRegister: DenyIncompleteValues if ($registerType -eq "AccumulationRegister") { $denyIncomplete = if ($parsed.flags -contains "denyincomplete") { "true" } else { "false" } $sb.AppendLine("$indent`t`t$denyIncomplete") | Out-Null } $indexing = "DontIndex" if ($parsed.flags -contains "index") { $indexing = "Index" } $sb.AppendLine("$indent`t`t$indexing") | Out-Null $sb.AppendLine("$indent`t`tUse") | Out-Null # AccumulationRegister: UseInTotals if ($registerType -eq "AccumulationRegister") { $useInTotals = if ($parsed.flags -contains "nouseintotals") { "false" } else { "true" } $sb.AppendLine("$indent`t`t$useInTotals") | Out-Null } # InformationRegister: DataHistory if ($registerType -eq "InformationRegister") { $sb.AppendLine("$indent`t`tUse") | Out-Null } $sb.AppendLine("$indent`t") | Out-Null $sb.Append("$indent") | Out-Null return $sb.ToString() } function Build-ResourceFragment { param($parsed, [string]$registerType, [string]$indent) if (-not $registerType) { $registerType = $script:objType } $uuid = New-Guid-String $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $typeStr = $parsed.type if ($typeStr) { $sb.AppendLine($(Build-ValueTypeXml "$indent`t`t" $typeStr)) | Out-Null } else { # Default: Number(15,2) $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t`txs:decimal") | Out-Null $sb.AppendLine("$indent`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t`t`t15") | Out-Null $sb.AppendLine("$indent`t`t`t`t2") | Out-Null $sb.AppendLine("$indent`t`t`t`tAny") | Out-Null $sb.AppendLine("$indent`t`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null # InformationRegister: FillFromFillingValue, FillValue if ($registerType -eq "InformationRegister") { $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } $fillChecking = "DontCheck" if ($parsed.flags -contains "req") { $fillChecking = "ShowError" } $sb.AppendLine("$indent`t`t$fillChecking") | Out-Null $sb.AppendLine("$indent`t`tItems") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null # InformationRegister: Indexing, FullTextSearch, DataHistory if ($registerType -eq "InformationRegister") { $sb.AppendLine("$indent`t`tDontIndex") | Out-Null $sb.AppendLine("$indent`t`tUse") | Out-Null $sb.AppendLine("$indent`t`tUse") | Out-Null } # AccumulationRegister: FullTextSearch if ($registerType -eq "AccumulationRegister") { $sb.AppendLine("$indent`t`tUse") | Out-Null } $sb.AppendLine("$indent`t") | Out-Null $sb.Append("$indent") | Out-Null return $sb.ToString() } function Build-EnumValueFragment { param($parsed, [string]$indent) $uuid = New-Guid-String $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $parsed.name)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.Append("$indent") | Out-Null return $sb.ToString() } function Build-ColumnFragment { param($colDef, [string]$indent) $uuid = New-Guid-String $name = "" $synonym = "" $indexing = "DontIndex" $references = @() if ($colDef -is [string]) { $name = "$colDef" $synonym = Split-CamelCase $name } else { $name = "$($colDef.name)" $synonym = if ($colDef.synonym) { "$($colDef.synonym)" } else { Split-CamelCase $name } if ($colDef.indexing) { $indexing = "$($colDef.indexing)" } if ($colDef.references) { $references = @($colDef.references) } } $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $name)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t$indexing") | Out-Null if ($references.Count -gt 0) { $sb.AppendLine("$indent`t`t") | Out-Null foreach ($ref in $references) { $sb.AppendLine("$indent`t`t`t$ref") | Out-Null } $sb.AppendLine("$indent`t`t") | Out-Null } else { $sb.AppendLine("$indent`t`t") | Out-Null } $sb.AppendLine("$indent`t") | Out-Null $sb.Append("$indent") | Out-Null return $sb.ToString() } function Build-SimpleChildFragment { param([string]$tagName, [string]$name, [string]$indent) # For Form, Template, Command — just a name wrapper $uuid = New-Guid-String $synonym = Split-CamelCase $name $sb = New-Object System.Text.StringBuilder $sb.AppendLine("$indent<$tagName uuid=`"$uuid`">") | Out-Null $sb.AppendLine("$indent`t") | Out-Null $sb.AppendLine("$indent`t`t$(Esc-Xml $name)") | Out-Null $sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null # Forms get additional properties if ($tagName -eq "Form") { $sb.AppendLine("$indent`t`tOrdinary") | Out-Null $sb.AppendLine("$indent`t`tfalse") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } if ($tagName -eq "Template") { $sb.AppendLine("$indent`t`tSpreadsheetDocument") | Out-Null } if ($tagName -eq "Command") { $sb.AppendLine("$indent`t`tFormNavigationPanelGoTo") | Out-Null $sb.AppendLine("$indent`t`tAuto") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null $sb.AppendLine("$indent`t`t") | Out-Null } $sb.AppendLine("$indent`t") | Out-Null $sb.Append("$indent") | Out-Null return $sb.ToString() } # ============================================================ # Section 7: Name uniqueness check # ============================================================ function Get-AllChildNames { $names = @{} if (-not $script:childObjectsEl) { return $names } foreach ($child in $script:childObjectsEl.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } $propsEl = $null foreach ($gc in $child.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { $propsEl = $gc; break } } if (-not $propsEl) { continue } foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { $n = $gc.InnerText.Trim() if ($n) { $names[$n] = $child.LocalName } break } } # Also check ChildObjects of TabularSections for nested names if ($child.LocalName -eq "TabularSection") { foreach ($tsCh in $child.ChildNodes) { if ($tsCh.NodeType -eq 'Element' -and $tsCh.LocalName -eq "ChildObjects") { foreach ($tsChild in $tsCh.ChildNodes) { if ($tsChild.NodeType -ne 'Element') { continue } $tsProps = $null foreach ($gc in $tsChild.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { $tsProps = $gc; break } } if ($tsProps) { foreach ($gc in $tsProps.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { # TS attr names don't conflict with top-level break } } } } } } } } return $names } # ============================================================ # Section 8: Context and allowed child types # ============================================================ $script:validChildTypes = @{ "Catalog" = @("attributes","tabularSections","forms","templates","commands") "Document" = @("attributes","tabularSections","forms","templates","commands") "ExchangePlan" = @("attributes","tabularSections","forms","templates","commands") "ChartOfAccounts" = @("attributes","tabularSections","forms","templates","commands") "ChartOfCharacteristicTypes" = @("attributes","tabularSections","forms","templates","commands") "ChartOfCalculationTypes" = @("attributes","tabularSections","forms","templates","commands") "BusinessProcess" = @("attributes","tabularSections","forms","templates","commands") "Task" = @("attributes","tabularSections","forms","templates","commands") "Report" = @("attributes","tabularSections","forms","templates","commands") "DataProcessor" = @("attributes","tabularSections","forms","templates","commands") "Enum" = @("enumValues","forms","templates","commands") "InformationRegister" = @("dimensions","resources","attributes","forms","templates","commands") "AccumulationRegister" = @("dimensions","resources","attributes","forms","templates","commands") "AccountingRegister" = @("dimensions","resources","attributes","forms","templates","commands") "CalculationRegister" = @("dimensions","resources","attributes","forms","templates","commands") "DocumentJournal" = @("columns","forms","templates","commands") "Constant" = @("forms") } # Canonical child order in ChildObjects $script:childOrder = @( "Resource", "Dimension", "Attribute", "TabularSection", "AccountingFlag", "ExtDimensionAccountingFlag", "EnumValue", "Column", "AddressingAttribute", "Recalculation", "Form", "Template", "Command" ) # Map from DSL child type to XML element name $script:childTypeToXmlTag = @{ "attributes" = "Attribute" "tabularSections" = "TabularSection" "dimensions" = "Dimension" "resources" = "Resource" "enumValues" = "EnumValue" "columns" = "Column" "forms" = "Form" "templates" = "Template" "commands" = "Command" } # ============================================================ # Section 9: DSL key normalization # ============================================================ function Resolve-OperationKey([string]$key) { $k = $key.ToLower().Trim() if ($script:operationSynonyms.ContainsKey($k)) { return $script:operationSynonyms[$k] } return $null } function Resolve-ChildTypeKey([string]$key) { $k = $key.ToLower().Trim() if ($script:childTypeSynonyms.ContainsKey($k)) { return $script:childTypeSynonyms[$k] } return $null } # ============================================================ # Section 9.5: Inline mode converter # ============================================================ function Split-ByCommaOutsideParens([string]$str) { $result = @() $depth = 0 $current = "" foreach ($ch in $str.ToCharArray()) { if ($ch -eq '(') { $depth++ } elseif ($ch -eq ')') { $depth-- } if ($ch -eq ',' -and $depth -eq 0) { $result += $current $current = "" } else { $current += $ch } } if ($current) { $result += $current } return ,$result } function Convert-InlineToDefinition([string]$operation, [string]$value) { # Parse operation: "add-attribute" → ("add", "attribute") $opParts = $operation -split '-', 2 $op = $opParts[0] # add, remove, modify, set $target = $opParts[1] # attribute, ts, owner, owners, property, etc. # Complex property targets $complexTargetMap = @{ "owner" = "Owners"; "owners" = "Owners" "registerRecord" = "RegisterRecords"; "registerRecords" = "RegisterRecords" "basedOn" = "BasedOn" "inputByString" = "InputByString" "dataLockField" = "DataLockFields"; "dataLockFields" = "DataLockFields" "registeredDocument" = "RegisteredDocuments"; "registeredDocuments" = "RegisteredDocuments" } if ($complexTargetMap.ContainsKey($target)) { $propName = $complexTargetMap[$target] $values = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) # For InputByString, auto-prefix with MetaType.Name. if ($propName -eq "InputByString") { $prefix = "$($script:objType).$($script:objName)." $values = @($values | ForEach-Object { if ($_ -notmatch '\.') { "$prefix$_" } elseif ($_ -notmatch '^(Catalog|Document|InformationRegister|AccumulationRegister|AccountingRegister|CalculationRegister|ChartOfCharacteristicTypes|ChartOfCalculationTypes|ChartOfAccounts|ExchangePlan|BusinessProcess|Task|Enum|Report|DataProcessor)\.') { "$prefix$_" } else { $_ } }) } $def = New-Object PSCustomObject $complexAction = if ($op -eq "set") { "set" } else { $op } $def | Add-Member -NotePropertyName "_complex" -NotePropertyValue @( @{ action = $complexAction; property = $propName; values = $values } ) return $def } # Предопределённые (Ext/Predefined.xml) — отдельный файл; строим { : { predefined: [...] } }. if ($target -eq 'predefined') { $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $inner = New-Object PSCustomObject $inner | Add-Member -NotePropertyName 'predefined' -NotePropertyValue $items $def = New-Object PSCustomObject $def | Add-Member -NotePropertyName $op -NotePropertyValue $inner return $def } # TS attribute operations: dot notation "TSName.AttrDef" if ($target -eq "ts-attribute") { $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) # Group by TS name $tsGroups = [ordered]@{} foreach ($item in $items) { $dotIdx = $item.IndexOf('.') if ($dotIdx -le 0) { Warn "Invalid ts-attribute format (expected TSName.AttrDef): $item" continue } $tsName = $item.Substring(0, $dotIdx).Trim() $rest = $item.Substring($dotIdx + 1).Trim() if (-not $tsGroups.Contains($tsName)) { $tsGroups[$tsName] = @() } $tsGroups[$tsName] += $rest } # Build: { modify: { tabularSections: { TSName: { add/remove/modify: ... } } } } $tsModObj = New-Object PSCustomObject foreach ($tsName in $tsGroups.Keys) { $tsChanges = New-Object PSCustomObject switch ($op) { "add" { $tsChanges | Add-Member -NotePropertyName "add" -NotePropertyValue $tsGroups[$tsName] } "remove" { $tsChanges | Add-Member -NotePropertyName "remove" -NotePropertyValue $tsGroups[$tsName] } "modify" { $attrModObj = New-Object PSCustomObject foreach ($elemDef in $tsGroups[$tsName]) { $colonIdx = $elemDef.IndexOf(':') if ($colonIdx -le 0) { Warn "Invalid modify format (expected Name: key=val): $elemDef" continue } $elemName = $elemDef.Substring(0, $colonIdx).Trim() $changesPart = $elemDef.Substring($colonIdx + 1).Trim() $changesObj = New-Object PSCustomObject $changePairs = Split-ByCommaOutsideParens $changesPart foreach ($cp in $changePairs) { $cp = $cp.Trim() $eqIdx = $cp.IndexOf('=') if ($eqIdx -gt 0) { $ck = $cp.Substring(0, $eqIdx).Trim() $cv = $cp.Substring($eqIdx + 1).Trim() $changesObj | Add-Member -NotePropertyName $ck -NotePropertyValue $cv } } $attrModObj | Add-Member -NotePropertyName $elemName -NotePropertyValue $changesObj } $tsChanges | Add-Member -NotePropertyName "modify" -NotePropertyValue $attrModObj } } $tsModObj | Add-Member -NotePropertyName $tsName -NotePropertyValue $tsChanges } $def = New-Object PSCustomObject $modifyObj = New-Object PSCustomObject $modifyObj | Add-Member -NotePropertyName "tabularSections" -NotePropertyValue $tsModObj $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj return $def } # Target → JSON DSL child type $targetMap = @{ "attribute" = "attributes" "ts" = "tabularSections" "dimension" = "dimensions" "resource" = "resources" "enumValue" = "enumValues" "column" = "columns" "form" = "forms" "template" = "templates" "command" = "commands" "property" = "properties" } $childType = $targetMap[$target] if (-not $childType) { Write-Error "Unknown inline target: $target" exit 1 } $def = New-Object PSCustomObject switch ($op) { "add" { $items = @() if ($childType -eq "tabularSections") { # TS format: "TSName: attr1_shorthand, attr2_shorthand, ..." $tsValues = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) foreach ($tsVal in $tsValues) { $colonIdx = $tsVal.IndexOf(':') if ($colonIdx -gt 0) { $tsName = $tsVal.Substring(0, $colonIdx).Trim() $attrsPart = $tsVal.Substring($colonIdx + 1).Trim() # Split attrs by comma (paren-aware), reassemble if part doesn't start with "Name:" $rawParts = Split-ByCommaOutsideParens $attrsPart $attrStrs = @() $current = "" foreach ($rp in $rawParts) { $rp = $rp.Trim() if ($current -and $rp -match '^[А-Яа-яЁёA-Za-z_]\w*\s*:') { $attrStrs += $current $current = $rp } elseif ($current) { $current += ", $rp" } else { $current = $rp } } if ($current) { $attrStrs += $current } $items += [PSCustomObject]@{ name = $tsName; attrs = $attrStrs } } else { # Just a name, no attrs $items += $tsVal } } } else { # Batch split by ;; $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } $addObj = New-Object PSCustomObject $addObj | Add-Member -NotePropertyName $childType -NotePropertyValue $items $def | Add-Member -NotePropertyName "add" -NotePropertyValue $addObj } "remove" { $items = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $removeObj = New-Object PSCustomObject $removeObj | Add-Member -NotePropertyName $childType -NotePropertyValue $items $def | Add-Member -NotePropertyName "remove" -NotePropertyValue $removeObj } "modify" { if ($childType -eq "properties") { # "CodeLength=11 ;; DescriptionLength=150" $kvPairs = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $propsObj = New-Object PSCustomObject foreach ($kv in $kvPairs) { $eqIdx = $kv.IndexOf('=') if ($eqIdx -gt 0) { $k = $kv.Substring(0, $eqIdx).Trim() $v = $kv.Substring($eqIdx + 1).Trim() $propsObj | Add-Member -NotePropertyName $k -NotePropertyValue $v } else { Warn "Invalid property format (expected Key=Value): $kv" } } $modifyObj = New-Object PSCustomObject $modifyObj | Add-Member -NotePropertyName "properties" -NotePropertyValue $propsObj $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj } else { # "ElementName: key=val, key=val ;; Element2: key=val" $elemDefs = @($value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $childModObj = New-Object PSCustomObject foreach ($elemDef in $elemDefs) { $colonIdx = $elemDef.IndexOf(':') if ($colonIdx -le 0) { Warn "Invalid modify format (expected Name: key=val): $elemDef" continue } $elemName = $elemDef.Substring(0, $colonIdx).Trim() $changesPart = $elemDef.Substring($colonIdx + 1).Trim() $changesObj = New-Object PSCustomObject $changePairs = Split-ByCommaOutsideParens $changesPart foreach ($cp in $changePairs) { $cp = $cp.Trim() $eqIdx = $cp.IndexOf('=') if ($eqIdx -gt 0) { $ck = $cp.Substring(0, $eqIdx).Trim() $cv = $cp.Substring($eqIdx + 1).Trim() $changesObj | Add-Member -NotePropertyName $ck -NotePropertyValue $cv } } $childModObj | Add-Member -NotePropertyName $elemName -NotePropertyValue $changesObj } $modifyObj = New-Object PSCustomObject $modifyObj | Add-Member -NotePropertyName $childType -NotePropertyValue $childModObj $def | Add-Member -NotePropertyName "modify" -NotePropertyValue $modifyObj } } } return $def } # ============================================================ # Section 10: ADD operations # ============================================================ function Find-InsertionPoint { param([string]$xmlTag, $parsed) # Returns $refNode for Insert-BeforeElement (null = append) if (-not $script:childObjectsEl) { return $null } # Positional: after/before if ($parsed.after) { $afterEl = Find-ElementByName $script:childObjectsEl $xmlTag $parsed.after if ($afterEl) { # Insert after = insert before the next element sibling $next = $afterEl.NextSibling while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling } if ($next -and $next.LocalName -eq $xmlTag) { return $next } return $null # append } else { Warn "after='$($parsed.after)': element '$($parsed.after)' not found in $xmlTag, appending" } } if ($parsed.before) { $beforeEl = Find-ElementByName $script:childObjectsEl $xmlTag $parsed.before if ($beforeEl) { return $beforeEl } Warn "before='$($parsed.before)': element '$($parsed.before)' not found in $xmlTag, appending" } # Default: after last element of this type, or in canonical position $lastOfType = Find-LastElementOfType $script:childObjectsEl $xmlTag if ($lastOfType) { $next = $lastOfType.NextSibling while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling } return $next # null means append (which is correct: after last of type) } # No elements of this type yet — find canonical position $tagIdx = [array]::IndexOf($script:childOrder, $xmlTag) if ($tagIdx -lt 0) { return $null } # Find first element of any type that comes AFTER in the canonical order for ($i = $tagIdx + 1; $i -lt $script:childOrder.Count; $i++) { $nextTag = $script:childOrder[$i] $firstOfNext = Find-FirstElementOfType $script:childObjectsEl $nextTag if ($firstOfNext) { return $firstOfNext } } return $null # append at end } function Process-Add($addDef) { $addDef.PSObject.Properties | ForEach-Object { $rawKey = $_.Name $items = $_.Value if ($rawKey -in @('predefined','предопределенные','предопределённые')) { Add-PredefinedItems $items return } $childType = Resolve-ChildTypeKey $rawKey if (-not $childType) { Warn "Unknown add child type: $rawKey" return } # Validate allowed $allowed = $script:validChildTypes[$script:objType] if ($allowed -and $childType -notin $allowed) { Warn "$childType not allowed for $($script:objType), skipping" return } $xmlTag = $script:childTypeToXmlTag[$childType] if (-not $xmlTag) { Warn "No XML tag mapping for $childType" return } Ensure-ChildObjectsOpen $indent = Get-ChildIndent $script:childObjectsEl $existingNames = Get-AllChildNames switch ($childType) { "attributes" { foreach ($item in $items) { $parsed = Parse-AttributeShorthand $item if ($existingNames.ContainsKey($parsed.name)) { Warn "Attribute '$($parsed.name)' already exists, skipping" continue } $context = Get-AttributeContext $fragmentXml = Build-AttributeFragment $parsed $context $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint "Attribute" $parsed foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added attribute: $($parsed.name)" $script:addCount++ $existingNames[$parsed.name] = "Attribute" } } "tabularSections" { foreach ($item in $items) { $tsName = if ($item -is [string]) { "$item" } else { "$($item.name)" } if ($existingNames.ContainsKey($tsName)) { Warn "TabularSection '$tsName' already exists, skipping" continue } $tsDef = if ($item -is [string]) { @{ name = $item } } else { $item } $fragmentXml = Build-TabularSectionFragment $tsDef $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint "TabularSection" @{ after = ""; before = "" } foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added tabular section: $tsName" $script:addCount++ $existingNames[$tsName] = "TabularSection" } } "dimensions" { foreach ($item in $items) { $parsed = Parse-AttributeShorthand $item if ($existingNames.ContainsKey($parsed.name)) { Warn "Dimension '$($parsed.name)' already exists, skipping" continue } $fragmentXml = Build-DimensionFragment $parsed $script:objType $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint "Dimension" $parsed foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added dimension: $($parsed.name)" $script:addCount++ $existingNames[$parsed.name] = "Dimension" } } "resources" { foreach ($item in $items) { $parsed = Parse-AttributeShorthand $item if ($existingNames.ContainsKey($parsed.name)) { Warn "Resource '$($parsed.name)' already exists, skipping" continue } $fragmentXml = Build-ResourceFragment $parsed $script:objType $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint "Resource" $parsed foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added resource: $($parsed.name)" $script:addCount++ $existingNames[$parsed.name] = "Resource" } } "enumValues" { foreach ($item in $items) { $parsed = Parse-EnumValueShorthand $item if ($existingNames.ContainsKey($parsed.name)) { Warn "EnumValue '$($parsed.name)' already exists, skipping" continue } $fragmentXml = Build-EnumValueFragment $parsed $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint "EnumValue" $parsed foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added enum value: $($parsed.name)" $script:addCount++ $existingNames[$parsed.name] = "EnumValue" } } "columns" { foreach ($item in $items) { $colName = if ($item -is [string]) { "$item" } else { "$($item.name)" } if ($existingNames.ContainsKey($colName)) { Warn "Column '$colName' already exists, skipping" continue } $fragmentXml = Build-ColumnFragment $item $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint "Column" @{ after = ""; before = "" } foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added column: $colName" $script:addCount++ $existingNames[$colName] = "Column" } } { $_ -in @("forms","templates","commands") } { $tagMap = @{ "forms" = "Form"; "templates" = "Template"; "commands" = "Command" } $tag = $tagMap[$childType] foreach ($item in $items) { $itemName = if ($item -is [string]) { "$item" } else { "$($item.name)" } if ($existingNames.ContainsKey($itemName)) { Warn "$tag '$itemName' already exists, skipping" continue } $fragmentXml = Build-SimpleChildFragment $tag $itemName $indent $nodes = Import-Fragment $fragmentXml $refNode = Find-InsertionPoint $tag @{ after = ""; before = "" } foreach ($node in $nodes) { Insert-BeforeElement $script:childObjectsEl $node $refNode $indent } Info "Added $($tag.ToLower()): $itemName" $script:addCount++ $existingNames[$itemName] = $tag } } } } } # ============================================================ # Section 11: REMOVE operations # ============================================================ function Process-Remove($removeDef) { $removeDef.PSObject.Properties | ForEach-Object { $rawKey = $_.Name $names = $_.Value $childType = Resolve-ChildTypeKey $rawKey if (-not $childType) { Warn "Unknown remove child type: $rawKey" return } if ($childType -eq "properties") { Warn "Cannot remove properties — use modify instead" return } $xmlTag = $script:childTypeToXmlTag[$childType] if (-not $xmlTag -or -not $script:childObjectsEl) { Warn "No ChildObjects or unknown tag for $childType" return } foreach ($name in $names) { $nameStr = "$name" $el = Find-ElementByName $script:childObjectsEl $xmlTag $nameStr if (-not $el) { Warn "$xmlTag '$nameStr' not found, skipping remove" continue } Remove-NodeWithWhitespace $el Info "Removed $($xmlTag.ToLower()): $nameStr" $script:removeCount++ } } # Collapse if empty Collapse-ChildObjectsIfEmpty } # ============================================================ # Section 12: MODIFY operations # ============================================================ function Modify-Properties($propsDef) { $propsDef.PSObject.Properties | ForEach-Object { $propName = $_.Name $propValue = $_.Value # Find the property element in Properties $propEl = $null foreach ($child in $script:propertiesEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { $propEl = $child break } } if (-not $propEl) { # create-if-missing: известное свойство создаём (порядок 1С терпит — append); неизвестное → ошибка (опечатка) if ($script:knownObjectProps -notcontains $propName) { Write-Error "modify-property: неизвестное свойство '$propName' — нет такого свойства объекта (опечатка?)" exit 1 } $newNodes = Import-Fragment "<$propName/>" if ($newNodes.Count -gt 0) { Insert-PropertyInOrder $script:propertiesEl $newNodes[0] $null $propName $propEl = $newNodes[0] } else { Warn "Property '$propName': could not create element" return } } # Complex property: Owners, RegisterRecords, BasedOn, InputByString if ($script:complexPropertyMap.ContainsKey($propName)) { $valuesList = @() if ($propValue -is [array]) { $valuesList = @($propValue | ForEach-Object { "$_" }) } else { $valuesList = @("$propValue" -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } Set-ComplexProperty $propName $valuesList return } # Handle boolean values $valueStr = "$propValue" if ($propValue -is [bool]) { $valueStr = if ($propValue) { "true" } else { "false" } } $propEl.InnerText = $valueStr Info "Modified property: $propName = $valueStr" $script:modifyCount++ } } function Modify-ChildElements($modifyDef, [string]$childType) { $xmlTag = $script:childTypeToXmlTag[$childType] if (-not $xmlTag -or -not $script:childObjectsEl) { Warn "No ChildObjects or unknown tag for $childType" return } $modifyDef.PSObject.Properties | ForEach-Object { $elemName = $_.Name $changes = $_.Value $el = Find-ElementByName $script:childObjectsEl $xmlTag $elemName if (-not $el) { Warn "$xmlTag '$elemName' not found for modify" return } # Find Properties inside the element $propsEl = $null foreach ($gc in $el.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Properties") { $propsEl = $gc; break } } if (-not $propsEl) { Warn "$xmlTag '$elemName': no Properties element found" return } $changes.PSObject.Properties | ForEach-Object { $changeProp = $_.Name $changeValue = $_.Value # TS child attribute operations (add/remove/modify attrs inside a TabularSection) if ($xmlTag -eq "TabularSection" -and $changeProp -in @("add","remove","modify")) { # Find ChildObjects inside this TS element $tsChildObjEl = $null foreach ($gc in $el.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "ChildObjects") { $tsChildObjEl = $gc; break } } switch ($changeProp) { "add" { if (-not $tsChildObjEl) { Warn "TS '$elemName' has no ChildObjects element, cannot add attributes" return } # Ensure ChildObjects is open (not self-closing empty) $hasTsChildElements = $false foreach ($ch in $tsChildObjEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $hasTsChildElements = $true; break } } if (-not $hasTsChildElements) { $tsCoIndent = Get-ChildIndent $el $tsCloseWs = $script:xmlDoc.CreateWhitespace("`r`n$tsCoIndent") $tsChildObjEl.AppendChild($tsCloseWs) | Out-Null } foreach ($attrDef in @($changeValue)) { $parsed = Parse-AttributeShorthand $attrDef $existing = Find-ElementByName $tsChildObjEl "Attribute" $parsed.name if ($existing) { Warn "Attribute '$($parsed.name)' already exists in TS '$elemName', skipping" continue } $tsAttrIndent = Get-ChildIndent $tsChildObjEl $tsAttrContext = if ($script:objType -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport")) { "processor-tabular" } else { "tabular" } $fragmentXml = Build-AttributeFragment $parsed $tsAttrContext $tsAttrIndent $nodes = Import-Fragment $fragmentXml $savedCO = $script:childObjectsEl $script:childObjectsEl = $tsChildObjEl $refNode = Find-InsertionPoint "Attribute" $parsed $script:childObjectsEl = $savedCO foreach ($node in $nodes) { Insert-BeforeElement $tsChildObjEl $node $refNode $tsAttrIndent } Info "Added attribute to TS '$elemName': $($parsed.name)" $script:addCount++ } } "remove" { if (-not $tsChildObjEl) { Warn "TS '$elemName' has no ChildObjects, cannot remove attributes" return } foreach ($attrName in @($changeValue)) { $attrEl = Find-ElementByName $tsChildObjEl "Attribute" "$attrName" if (-not $attrEl) { Warn "Attribute '$attrName' not found in TS '$elemName', skipping" continue } Remove-NodeWithWhitespace $attrEl Info "Removed attribute from TS '$elemName': $attrName" $script:removeCount++ } } "modify" { if (-not $tsChildObjEl) { Warn "TS '$elemName' has no ChildObjects, cannot modify attributes" return } # Temporarily swap childObjectsEl and recurse $savedChildObjEl = $script:childObjectsEl $script:childObjectsEl = $tsChildObjEl Modify-ChildElements $changeValue "attributes" $script:childObjectsEl = $savedChildObjEl } } return # Skip normal property modification } switch ($changeProp) { "name" { # Rename $nameEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") { $nameEl = $gc; break } } if ($nameEl) { $oldName = $nameEl.InnerText.Trim() $newName = "$changeValue" $nameEl.InnerText = $newName # Update Synonym if it was auto-generated (matches old CamelCase split) $oldSynonym = Split-CamelCase $oldName $synEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Synonym") { $synEl = $gc; break } } if ($synEl) { # Check if current synonym matches auto-generated from old name $currentSyn = "" foreach ($item in $synEl.ChildNodes) { if ($item.NodeType -eq 'Element' -and $item.LocalName -eq "item") { foreach ($gc in $item.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "content") { $currentSyn = $gc.InnerText.Trim() } } } } if ($currentSyn -eq $oldSynonym -or -not $currentSyn) { $newSynonym = Split-CamelCase $newName $synXml = Build-MLTextXml (Get-ChildIndent $propsEl) "Synonym" $newSynonym $newSynNodes = Import-Fragment $synXml if ($newSynNodes.Count -gt 0) { $propsEl.InsertAfter($newSynNodes[0], $synEl) | Out-Null Remove-NodeWithWhitespace $synEl } } } Info "Renamed ${xmlTag}: $oldName -> $newName" $script:modifyCount++ } } "type" { # Change type $typeEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Type") { $typeEl = $gc; break } } $newTypeStr = "$changeValue" $typeIndent = Get-ChildIndent $propsEl $newTypeXml = Build-ValueTypeXml $typeIndent $newTypeStr $newTypeNodes = Import-Fragment $newTypeXml if ($typeEl -and $newTypeNodes.Count -gt 0) { $propsEl.InsertAfter($newTypeNodes[0], $typeEl) | Out-Null Remove-NodeWithWhitespace $typeEl } elseif ($newTypeNodes.Count -gt 0) { # No existing Type — insert after Comment $commentEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Comment") { $commentEl = $gc; break } } if ($commentEl) { Insert-BeforeElement $propsEl $newTypeNodes[0] $commentEl.NextSibling $typeIndent } } # Also update FillValue if present $fillValEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "FillValue") { $fillValEl = $gc; break } } if ($fillValEl) { $fillIndent = Get-ChildIndent $propsEl $newFillXml = Build-FillValueXml $fillIndent $newTypeStr $newFillNodes = Import-Fragment $newFillXml if ($newFillNodes.Count -gt 0) { $propsEl.InsertAfter($newFillNodes[0], $fillValEl) | Out-Null Remove-NodeWithWhitespace $fillValEl } } Info "Changed type of $xmlTag '$elemName': $newTypeStr" $script:modifyCount++ } "synonym" { $synEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Synonym") { $synEl = $gc; break } } $synIndent = Get-ChildIndent $propsEl $newSynXml = Build-MLTextXml $synIndent "Synonym" "$changeValue" $newSynNodes = Import-Fragment $newSynXml if ($synEl -and $newSynNodes.Count -gt 0) { $propsEl.InsertAfter($newSynNodes[0], $synEl) | Out-Null Remove-NodeWithWhitespace $synEl } Info "Changed synonym of $xmlTag '$elemName': $changeValue" $script:modifyCount++ } "Format" { if (Set-AttrPropertyElement $propsEl "Format" (Build-MLTextXml (Get-ChildIndent $propsEl) "Format" "$changeValue")) { Info "Set $xmlTag '$elemName'.Format"; $script:modifyCount++ } } "EditFormat" { if (Set-AttrPropertyElement $propsEl "EditFormat" (Build-MLTextXml (Get-ChildIndent $propsEl) "EditFormat" "$changeValue")) { Info "Set $xmlTag '$elemName'.EditFormat"; $script:modifyCount++ } } "ToolTip" { if (Set-AttrPropertyElement $propsEl "ToolTip" (Build-MLTextXml (Get-ChildIndent $propsEl) "ToolTip" "$changeValue")) { Info "Set $xmlTag '$elemName'.ToolTip"; $script:modifyCount++ } } "ChoiceForm" { if (Set-AttrPropertyElement $propsEl "ChoiceForm" "$(Esc-Xml "$changeValue")") { Info "Set $xmlTag '$elemName'.ChoiceForm"; $script:modifyCount++ } } "MinValue" { if (Set-AttrPropertyElement $propsEl "MinValue" (Build-MinMaxValueXml "MinValue" $changeValue)) { Info "Set $xmlTag '$elemName'.MinValue"; $script:modifyCount++ } } "MaxValue" { if (Set-AttrPropertyElement $propsEl "MaxValue" (Build-MinMaxValueXml "MaxValue" $changeValue)) { Info "Set $xmlTag '$elemName'.MaxValue"; $script:modifyCount++ } } "LinkByType" { if (Set-AttrPropertyElement $propsEl "LinkByType" (Build-LinkByTypeXml (Get-ChildIndent $propsEl) $changeValue)) { Info "Set $xmlTag '$elemName'.LinkByType"; $script:modifyCount++ } } "ChoiceParameterLinks" { if (Set-AttrPropertyElement $propsEl "ChoiceParameterLinks" (Build-ChoiceParameterLinksXml (Get-ChildIndent $propsEl) $changeValue)) { Info "Set $xmlTag '$elemName'.ChoiceParameterLinks"; $script:modifyCount++ } } "ChoiceParameters" { if (Set-AttrPropertyElement $propsEl "ChoiceParameters" (Build-ChoiceParametersXml (Get-ChildIndent $propsEl) $changeValue)) { Info "Set $xmlTag '$elemName'.ChoiceParameters"; $script:modifyCount++ } } "FillValue" { if (Set-AttrPropertyElement $propsEl "FillValue" (Build-FillValueExplicitXml (Get-AttrTypeStrFromXml $propsEl) $changeValue)) { Info "Set $xmlTag '$elemName'.FillValue"; $script:modifyCount++ } } default { # Scalar property change (Indexing, FillChecking, Use, etc.) $scalarEl = $null foreach ($gc in $propsEl.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $changeProp) { $scalarEl = $gc; break } } if ($scalarEl) { $valueStr = "$changeValue" if ($changeValue -is [bool]) { $valueStr = if ($changeValue) { "true" } else { "false" } } else { $valueStr = Normalize-EnumValue $changeProp $valueStr } $scalarEl.InnerText = $valueStr Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr" $script:modifyCount++ } else { # create-if-missing: известное свойство создаём в позиции; неизвестное → ошибка (опечатка) if ($script:knownChildProps -notcontains $changeProp) { Write-Error "modify: неизвестное свойство '$changeProp' у $xmlTag '$elemName' (опечатка?)" exit 1 } $valueStr = "$changeValue" if ($changeValue -is [bool]) { $valueStr = if ($changeValue) { "true" } else { "false" } } else { $valueStr = Normalize-EnumValue $changeProp $valueStr } $newNodes = Import-Fragment "<$changeProp>$(Esc-Xml $valueStr)" if ($newNodes.Count -gt 0) { Insert-PropertyInOrder $propsEl $newNodes[0] $script:attrPropOrder $changeProp Info "Created $xmlTag '$elemName'.$changeProp = $valueStr" $script:modifyCount++ } } } } } } } function Process-Modify($modifyDef) { $modifyDef.PSObject.Properties | ForEach-Object { $rawKey = $_.Name $value = $_.Value $childType = Resolve-ChildTypeKey $rawKey if (-not $childType) { Warn "Unknown modify child type: $rawKey" return } if ($childType -eq "properties") { Modify-Properties $value } else { Modify-ChildElements $value $childType } } } # ============================================================ # Section 12.5: Complex property helpers # ============================================================ $script:complexPropertyMap = @{ "Owners" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } "RegisterRecords" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } "BasedOn" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } "InputByString" = @{ tag = "xr:Field"; attr = $null } "DataLockFields" = @{ tag = "xr:Field"; attr = $null; expand = $true } "RegisteredDocuments" = @{ tag = "xr:Item"; attr = 'xsi:type="xr:MDObjectRef"' } } # Известные свойства объекта (union по корпусу acc+erp 8.3.24) — allowlist для modify-property. # Известное отсутствующее свойство create-if-missing создаётся; неизвестное (опечатка) → ошибка. $script:knownObjectProps = @( 'ActionPeriod','ActionPeriodUse','Addressing','AutoOrderByCode','Autonumbering','AuxiliaryChoiceForm', 'AuxiliaryFolderChoiceForm','AuxiliaryFolderForm','AuxiliaryForm','AuxiliaryListForm','AuxiliaryObjectForm', 'AuxiliaryRecordForm','AuxiliarySettingsForm','BaseCalculationTypes','BasePeriod','BasedOn', 'CharacteristicExtValues','Characteristics','ChartOfAccounts','ChartOfCalculationTypes','CheckUnique', 'ChoiceDataGetModeOnInputByString','ChoiceFoldersAndItems','ChoiceForm','ChoiceHistoryOnInput','ChoiceMode', 'ChoiceParameterLinks','ChoiceParameters','CodeAllowedLength','CodeLength','CodeMask','CodeSeries','CodeType', 'Comment','Correspondence','CreateOnInput','CreateTaskInPrivilegedMode','CurrentPerformer','DataHistory', 'DataLockControlMode','DataLockFields','DefaultChoiceForm','DefaultFolderChoiceForm','DefaultFolderForm', 'DefaultForm','DefaultListForm','DefaultObjectForm','DefaultPresentation','DefaultRecordForm','DefaultSettingsForm', 'DefaultVariantForm','DependenceOnCalculationTypes','DescriptionLength','DistributedInfoBase','EditFormat', 'EditType','EnableTotalsSliceFirst','EnableTotalsSliceLast','EnableTotalsSplitting', 'ExecuteAfterWriteDataHistoryVersionProcessing','Explanation','ExtDimensionTypes','ExtendedEdit', 'ExtendedListPresentation','ExtendedObjectPresentation','ExtendedPresentation','ExtendedRecordPresentation', 'FillChecking','FoldersOnTop','Format','FullTextSearch','FullTextSearchOnInputByString','Hierarchical', 'HierarchyType','IncludeConfigurationExtensions','IncludeHelpInContents','InformationRegisterPeriodicity', 'InputByString','LevelCount','LimitLevelCount','LinkByType','ListPresentation','MainAddressingAttribute', 'MainDataCompositionSchema','MainFilterOnPeriod','MarkNegatives','Mask','MaxExtDimensionCount','MaxValue', 'MinValue','MultiLine','Name','NumberAllowedLength','NumberLength','NumberPeriodicity','NumberType','Numerator', 'ObjectPresentation','OrderLength','Owners','PasswordMode','PeriodAdjustmentLength','Periodicity', 'PostInPrivilegedMode','Posting','PredefinedDataUpdate','QuickChoice','RealTimePosting','RecordPresentation', 'RegisterRecords','RegisterRecordsDeletion','RegisterRecordsWritingOnPost','RegisterType','RegisteredDocuments', 'Schedule','ScheduleDate','ScheduleValue','SearchStringModeOnInputByString','SequenceFilling','SettingsStorage', 'StandardAttributes','StandardTabularSections','SubordinationUse','Synonym','Task','TaskNumberAutoPrefix', 'ToolTip','Type','UnpostInPrivilegedMode','UpdateDataHistoryImmediatelyAfterWrite','UseStandardCommands', 'VariantsStorage','WriteMode' ) # Известные свойства дочерних элементов (union Attribute/Dimension/Resource по корпусу) — allowlist default-ветки # modify-attribute/-dimension/-resource. $script:knownChildProps = @( 'AccountingFlag','Balance','BaseDimension','ChoiceFoldersAndItems','ChoiceForm','ChoiceHistoryOnInput', 'ChoiceParameterLinks','ChoiceParameters','Comment','CreateOnInput','DataHistory','DenyIncompleteValues', 'DocumentMap','EditFormat','ExtDimensionAccountingFlag','ExtendedEdit','FillChecking','FillFromFillingValue', 'FillValue','Format','FullTextSearch','Indexing','LinkByType','MainFilter','MarkNegatives','Mask','Master', 'MaxValue','MinValue','MultiLine','Name','PasswordMode','QuickChoice','RegisterRecordsMap','ScheduleLink', 'Synonym','ToolTip','Type','Use','UseInTotals' ) # Канонический порядок свойств реквизита (последовательность Build-AttributeFragment) — для вставки в позицию. # Порядок 1С терпит (cert), но держим канонический для консистентности с meta-compile. $script:attrPropOrder = @( 'Name','Synonym','Comment','Type','PasswordMode','Format','EditFormat','ToolTip','MarkNegatives','Mask', 'MultiLine','ExtendedEdit','MinValue','MaxValue','FillFromFillingValue','FillValue','FillChecking', 'ChoiceFoldersAndItems','ChoiceParameterLinks','ChoiceParameters','QuickChoice','CreateOnInput','ChoiceForm', 'LinkByType','ChoiceHistoryOnInput','Use','Indexing','FullTextSearch','DataHistory' ) # Вставить новый элемент свойства в Properties в канонической позиции (по orderArray); если свойства нет в # orderArray (или orderArray пуст) — append. Порядок 1С терпит, канонический — для консистентности/снапшотов. function Insert-PropertyInOrder($propsEl, $newNode, $orderArray, $propName) { $childIndent = "$(Get-ChildIndent $propsEl)" $refNode = $null $idx = if ($orderArray) { [array]::IndexOf($orderArray, $propName) } else { -1 } if ($idx -ge 0) { foreach ($ch in $propsEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $ci = [array]::IndexOf($orderArray, $ch.LocalName) if ($ci -gt $idx) { $refNode = $ch; break } } } } Insert-BeforeElement $propsEl $newNode $refNode $childIndent } # Заменить существующий элемент свойства реквизита новым фрагментом (по образцу ветки type), # либо создать в канонической позиции, если его нет. Возвращает $true при успехе. function Set-AttrPropertyElement($propsEl, $propName, $fragmentXml) { $newNodes = Import-Fragment $fragmentXml if ($newNodes.Count -eq 0) { return $false } $existing = $null foreach ($ch in $propsEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $propName) { $existing = $ch; break } } if ($existing) { # InsertBefore+RemoveChild сохраняет ведущий/хвостовой whitespace позиции existing # (InsertAfter+Remove-NodeWithWhitespace склеил бы: удаляет ведущий ws как отдельный узел). $propsEl.InsertBefore($newNodes[0], $existing) | Out-Null $propsEl.RemoveChild($existing) | Out-Null } else { Insert-PropertyInOrder $propsEl $newNodes[0] $script:attrPropOrder $propName } return $true } # MinValue/MaxValue — типизированное значение (порт Emit-MinMaxValue): nil / xs:string / xs:decimal. function Build-MinMaxValueXml([string]$tag, $val) { if ($null -eq $val -or "$val" -eq '') { return "<$tag xsi:nil=`"true`"/>" } $t = if ($val -is [string]) { 'xs:string' } else { 'xs:decimal' } return "<$tag xsi:type=`"$t`">$(Esc-Xml "$val")" } # --- Порт из meta-compile: развёртка путей данных + связи выбора / тип по ссылке (structural modify) --- # Свойство из dict/PSCustomObject по списку синонимов (первый найденный, иначе $null). function Get-ChElProp($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 } # Стандартный реквизит рус/англ → английский (для Catalog/Document); использует существующие reserved-карты. function Resolve-StdAttrEn([string]$name) { $ctx = switch ("$script: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 } # Прощающий ввод пути данных: короткое имя реквизита → полный путь объекта (порт Expand-DataPath). function Expand-DataPath([string]$dp) { if (-not $dp) { return $dp } $s = "$dp" if ($s -match '[:/]') { return $s } if ($s -match '^-?\d+$') { return $s } if ($s -match '^(StandardAttribute|Attribute)\.') { return "$($script:objType).$($script:objName).$s" } if (-not $s.Contains('.')) { $en = Resolve-StdAttrEn $s if ($en) { return "$($script:objType).$($script:objName).StandardAttribute.$en" } return "$($script:objType).$($script:objName).Attribute.$s" } return $s } # Shorthand "name=path" | "name=path:Clear|DontChange" → {name, dataPath, valueChange?}. function ConvertFrom-ChLinkShorthand([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 } # LinkByType — {dataPath, linkItem?} (порт Emit-LinkByType). Строка → dataPath, linkItem=0. function Build-LinkByTypeXml([string]$indent, $spec) { if (-not $spec) { return "$indent" } if ($spec -is [string]) { $dp = "$spec"; $li = 0 } else { $dp = "$(Get-ChElProp $spec @('dataPath','path','путь'))" $liRaw = Get-ChElProp $spec @('linkItem','элементСвязи') $li = if ($null -ne $liRaw) { $liRaw } else { 0 } } if (-not $dp) { return "$indent" } $dp = Expand-DataPath $dp $lines = @( "$indent" "$indent`t$(Esc-Xml "$dp")" "$indent`t$li" "$indent" ) return $lines -join "`r`n" } # ChoiceParameterLinks — [{name, dataPath, valueChange?}] (порт Emit-ChoiceParameterLinks). valueChange дефолт Clear. function Build-ChoiceParameterLinksXml([string]$indent, $cpl) { if (-not $cpl -or @($cpl).Count -eq 0) { return "$indent" } $sb = New-Object System.Text.StringBuilder $sb.Append("$indent") | Out-Null 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" } } } $sb.Append("`r`n$indent`t") | Out-Null $sb.Append("`r`n$indent`t`t$(Esc-Xml "$name")") | Out-Null $sb.Append("`r`n$indent`t`t$(Esc-Xml "$dp")") | Out-Null $sb.Append("`r`n$indent`t`t$vc") | Out-Null $sb.Append("`r`n$indent`t") | Out-Null } $sb.Append("`r`n$indent") | Out-Null return $sb.ToString() } # --- Порт из meta-compile: значения параметров выбора (ChoiceParameters) --- $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:accountTypeValues = @('Active','Passive','ActivePassive') $script:fillRefKindRoot = @{ 'catalogref'='Catalog'; 'documentref'='Document'; 'enumref'='Enum'; 'chartofaccountsref'='ChartOfAccounts'; 'chartofcharacteristictypesref'='ChartOfCharacteristicTypes'; 'chartofcalculationtypesref'='ChartOfCalculationTypes'; 'exchangeplanref'='ExchangePlan'; 'businessprocessref'='BusinessProcess'; 'taskref'='Task' } function ConvertTo-ChScalar([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 } function Format-FillNum($n) { if ($n -is [double] -or $n -is [decimal]) { return $n.ToString([System.Globalization.CultureInfo]::InvariantCulture) } return "$n" } function Normalize-FillRef([string]$s) { if ([string]::IsNullOrEmpty($s)) { return $null } 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 } 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" } $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 '.') } function Expand-ChoiceRefValue([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" } function Normalize-ChoiceValue($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 } } function Normalize-ChoiceValueT($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 } function ConvertFrom-ChParamShorthand([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) } } # ChoiceParameters — [{name, type?, value?}] (порт Emit-ChoiceParameters). Значение на app:value (xsi:type=тип); # массив → v8:FixedArray с v8:Value; без value → nil. Требует xmlns:app в Import-Fragment. function Build-ChoiceParametersXml([string]$indent, $cp) { if (-not $cp -or @($cp).Count -eq 0) { return "$indent" } $sb = New-Object System.Text.StringBuilder $sb.Append("$indent") | Out-Null 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]) $sb.Append("`r`n$indent`t") | Out-Null if (-not $hasVal) { $sb.Append("`r`n$indent`t`t") | Out-Null } elseif ($valIsArray) { $sb.Append("`r`n$indent`t`t") | Out-Null foreach ($v in $val) { $norm = Normalize-ChoiceValueT $v $ptype if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t`t") | Out-Null } else { $sb.Append("`r`n$indent`t`t`t$(Esc-Xml $norm.Text)") | Out-Null } } $sb.Append("`r`n$indent`t`t") | Out-Null } else { $norm = Normalize-ChoiceValueT $val $ptype if ([string]::IsNullOrEmpty($norm.Text)) { $sb.Append("`r`n$indent`t`t") | Out-Null } else { $sb.Append("`r`n$indent`t`t$(Esc-Xml $norm.Text)") | Out-Null } } $sb.Append("`r`n$indent`t") | Out-Null } $sb.Append("`r`n$indent") | Out-Null return $sb.ToString() } # --- Порт из meta-compile: явное значение заполнения (FillValue) --- $script:fillBoolTrue = @('true','истина','да') $script:fillBoolFalse = @('false','ложь','нет') function Esc-XmlText([string]$s) { return $s.Replace('&','&').Replace('<','<').Replace('>','>') } function Get-FillTypeCategory([string]$typeStr) { if (-not $typeStr) { return 'String' } if ($typeStr -match '\+') { return 'Other' } $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' } function Expand-FillShortRef([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" } function Resolve-FillValueSpec([string]$s, [string]$typeStr) { $cat = Get-FillTypeCategory $typeStr if ($s -eq '') { return @{ XsiType='xs:string'; Text='' } } if ($cat -eq 'String') { return @{ XsiType='xs:string'; Text=$s } } 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 } } 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 } } $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 } } # Извлечь тип реквизита из XML (/) → DSL-typeStr для категоризации FillValue. function Get-AttrTypeStrFromXml($propsEl) { $typeEl = $null foreach ($ch in $propsEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'Type') { $typeEl = $ch; break } } if (-not $typeEl) { return "" } $mapped = @() foreach ($ch in $typeEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'Type') { $t = $ch.InnerText.Trim() $colon = $t.IndexOf(':'); if ($colon -ge 0) { $t = $t.Substring($colon + 1) } switch -Regex ($t) { '^string$' { $mapped += 'String'; break } '^decimal$' { $mapped += 'Number'; break } '^boolean$' { $mapped += 'Boolean'; break } '^dateTime$' { $mapped += 'Date'; break } default { $mapped += $t } } } } if ($mapped.Count -eq 0) { return "" } if ($mapped.Count -gt 1) { return ($mapped -join ' + ') } return $mapped[0] } # FillValue — явное значение (порт Emit-FillValue, ветка hasSpec). Маркеры {nil}/{emptyRef}; иначе по типу. function Build-FillValueExplicitXml([string]$typeStr, $spec) { if ($null -eq $spec) { return "" } if ($spec -is [bool]) { return "$(if ($spec) { 'true' } else { 'false' })" } if ($spec -is [int] -or $spec -is [long] -or $spec -is [double] -or $spec -is [decimal]) { return "$(Format-FillNum $spec)" } if ((Get-ChElProp $spec @('nil')) -eq $true) { return "" } if ((Get-ChElProp $spec @('emptyRef','пустаяссылка')) -eq $true) { return "" } $r = Resolve-FillValueSpec "$spec" $typeStr if ($r.Text -eq '' -and $r.XsiType -eq 'xs:string') { return "" } return "$(Esc-XmlText $r.Text)" } function Find-PropertyElement([string]$propName) { foreach ($child in $script:propertiesEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { return $child } } return $null } function Get-ComplexPropertyValues([System.Xml.XmlElement]$propEl) { $values = @() foreach ($child in $propEl.ChildNodes) { if ($child.NodeType -eq 'Element') { $values += $child.InnerText.Trim() } } return $values } function Add-ComplexPropertyItem([string]$propertyName, [string[]]$values) { $mapEntry = $script:complexPropertyMap[$propertyName] if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) } $propEl = Find-PropertyElement $propertyName if (-not $propEl) { Warn "Property element '$propertyName' not found in Properties" return } # Get existing values to check duplicates $existing = Get-ComplexPropertyValues $propEl $indent = Get-ChildIndent $script:propertiesEl $childIndent = "$indent`t" # Check if element is self-closing (empty) $isEmpty = $true foreach ($ch in $propEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $isEmpty = $false; break } } # If self-closing / empty, add closing whitespace if ($isEmpty -and $propEl.ChildNodes.Count -eq 0) { $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") $propEl.AppendChild($closeWs) | Out-Null } foreach ($val in $values) { if ($val -in $existing) { Warn "$propertyName already contains '$val', skipping" continue } $tag = $mapEntry.tag $attrStr = $mapEntry.attr if ($attrStr) { $fragXml = "<$tag $attrStr>$(Esc-Xml $val)" } else { $fragXml = "<$tag>$(Esc-Xml $val)" } $nodes = Import-Fragment $fragXml foreach ($node in $nodes) { Insert-BeforeElement $propEl $node $null $childIndent } Info "Added $propertyName item: $val" $script:addCount++ } } function Remove-ComplexPropertyItem([string]$propertyName, [string[]]$values) { $mapEntry = $script:complexPropertyMap[$propertyName] if ($mapEntry -and $mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) } $propEl = Find-PropertyElement $propertyName if (-not $propEl) { Warn "Property element '$propertyName' not found in Properties" return } foreach ($val in $values) { $found = $false foreach ($child in @($propEl.ChildNodes)) { if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $val) { Remove-NodeWithWhitespace $child Info "Removed $propertyName item: $val" $script:removeCount++ $found = $true break } } if (-not $found) { Warn "$propertyName item '$val' not found, skipping" } } # Collapse if empty $hasElements = $false foreach ($ch in $propEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } } if (-not $hasElements) { while ($propEl.HasChildNodes) { $propEl.RemoveChild($propEl.FirstChild) | Out-Null } } } function Set-ComplexProperty([string]$propertyName, [string[]]$values) { $mapEntry = $script:complexPropertyMap[$propertyName] if (-not $mapEntry) { Warn "Unknown complex property: $propertyName"; return } if ($mapEntry.expand) { $values = @($values | ForEach-Object { Expand-DataPath "$_" }) } $propEl = Find-PropertyElement $propertyName if (-not $propEl) { Warn "Property element '$propertyName' not found in Properties" return } $indent = Get-ChildIndent $script:propertiesEl $childIndent = "$indent`t" # Remove all existing children while ($propEl.HasChildNodes) { $propEl.RemoveChild($propEl.FirstChild) | Out-Null } if ($values.Count -eq 0) { # Leave self-closing Info "Cleared $propertyName" $script:modifyCount++ return } # Add closing whitespace $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$indent") $propEl.AppendChild($closeWs) | Out-Null # Add each value foreach ($val in $values) { $tag = $mapEntry.tag $attrStr = $mapEntry.attr if ($attrStr) { $fragXml = "<$tag $attrStr>$(Esc-Xml $val)" } else { $fragXml = "<$tag>$(Esc-Xml $val)" } $nodes = Import-Fragment $fragXml foreach ($node in $nodes) { Insert-BeforeElement $propEl $node $null $childIndent } } $count = $values.Count Info "Set $propertyName`: $count items" $script:modifyCount++ } # ============================================================ # Section 13: Main processing # ============================================================ # ============================================================ # Predefined data (Ext/Predefined.xml) — add предопределённых (Catalog/ChartOfCharacteristicTypes). # Существующие сохраняются побайтово (текстовый append), новые получают свежий GUID — # инвариант «не менять id существующей сущности». # ============================================================ $script:predefXsiTypeByObj = @{ 'Catalog' = 'CatalogPredefinedItems' 'ChartOfCharacteristicTypes' = 'PlanOfCharacteristicKindPredefinedItems' } function Get-PredefinedPath { $objDir = Join-Path (Split-Path $resolvedPath) $script:objName return (Join-Path (Join-Path $objDir "Ext") "Predefined.xml") } function Get-ObjectCodeType { foreach ($ch in $script:propertiesEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'CodeType') { return $ch.InnerText.Trim() } } return 'String' } # Элемент DSL: строка "(Код) Имя [Наименование]" ЛИБО объект {name,code,description,isFolder,childItems}. function Resolve-PredefItem($val) { if ($val -is [string]) { $s = "$val"; $descRaw = $null; $hasDesc = $false if ($s -match '\[(.*)\]') { $descRaw = $Matches[1]; $hasDesc = $true; $s = $s -replace '\s*\[.*\]', '' } $m = [regex]::Match($s.Trim(), '^\s*(?:\(([^)]*)\)\s*)?(\S+)\s*$') $name = $m.Groups[2].Value $code = if ($m.Groups[1].Success) { $m.Groups[1].Value } else { '' } $desc = if ($hasDesc) { $descRaw } else { Split-CamelCase $name } return @{ name = $name; code = $code; desc = $desc; isFolder = $false; children = @() } } $gv = { param($o, [string[]]$keys) foreach ($k in $keys) { if ($o.PSObject.Properties[$k]) { return $o.$k } } return $null } $name = "$(& $gv $val @('name','имя'))" $codeV = & $gv $val @('code','код'); $code = if ($null -ne $codeV) { "$codeV" } else { '' } $hasDesc = $val.PSObject.Properties['description'] -or $val.PSObject.Properties['наименование'] $descV = & $gv $val @('description','наименование') $desc = if ($hasDesc) { "$descV" } else { Split-CamelCase $name } $isFolder = ((& $gv $val @('isFolder','группа')) -eq $true) $subs = & $gv $val @('childItems','подчиненные') return @{ name = $name; code = $code; desc = $desc; isFolder = $isFolder; children = @(if ($subs) { @($subs) } else { @() }) } } function Build-PredefItemXml([string]$indent, $val, [string]$codeType) { $r = Resolve-PredefItem $val $sb = New-Object System.Text.StringBuilder [void]$sb.Append("$indent`r`n") [void]$sb.Append("$indent`t$(Esc-XmlText $r.name)`r`n") if (-not $r.code) { [void]$sb.Append("$indent`t`r`n") } elseif ($codeType -eq 'Number') { [void]$sb.Append("$indent`t$(Esc-XmlText $r.code)`r`n") } else { [void]$sb.Append("$indent`t$(Esc-XmlText $r.code)`r`n") } if ($r.desc -eq '') { [void]$sb.Append("$indent`t`r`n") } else { [void]$sb.Append("$indent`t$(Esc-XmlText $r.desc)`r`n") } [void]$sb.Append("$indent`t$(if ($r.isFolder) { 'true' } else { 'false' })`r`n") if ($r.children.Count -gt 0) { [void]$sb.Append("$indent`t`r`n") foreach ($c in $r.children) { [void]$sb.Append((Build-PredefItemXml "$indent`t`t" $c $codeType)) } [void]$sb.Append("$indent`t`r`n") } [void]$sb.Append("$indent`r`n") return $sb.ToString() } function Add-PredefinedItems($items) { $xsiType = $script:predefXsiTypeByObj[$script:objType] if (-not $xsiType) { Write-Error "add-predefined: тип объекта '$($script:objType)' не поддержан (только Catalog, ChartOfCharacteristicTypes)"; exit 1 } $codeType = Get-ObjectCodeType $version = $script:xmlDoc.DocumentElement.GetAttribute("version") $path = Get-PredefinedPath $itemsXml = "" foreach ($it in @($items)) { $itemsXml += (Build-PredefItemXml "`t" $it $codeType) } $utf8Bom = New-Object System.Text.UTF8Encoding($true) if (Test-Path $path) { $text = [System.IO.File]::ReadAllText($path, $utf8Bom) $text = $text.Replace("", "$itemsXml") } else { $extDir = Split-Path $path if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } $hdr = "`r`n`r`n" $text = "$hdr$itemsXml`r`n" } [System.IO.File]::WriteAllText($path, $text, $utf8Bom) $n = @($items).Count Info "Added $n predefined item(s) → $path" $script:addCount += $n } # --- Inline mode conversion --- if ($Operation) { $def = Convert-InlineToDefinition $Operation $Value } if (-not $def) { Write-Error "No definition loaded" exit 1 } # --- Process complex property operations --- if ($def.PSObject.Properties.Match("_complex").Count -gt 0 -and $def._complex) { foreach ($cop in $def._complex) { switch ($cop.action) { "add" { Add-ComplexPropertyItem $cop.property $cop.values } "remove" { Remove-ComplexPropertyItem $cop.property $cop.values } "set" { Set-ComplexProperty $cop.property $cop.values } } } } # --- Process standard operations --- $def.PSObject.Properties | ForEach-Object { $prop = $_ if ($prop.Name -eq "_complex") { return } $opKey = Resolve-OperationKey $prop.Name if (-not $opKey) { Warn "Unknown operation: $($prop.Name)" return } switch ($opKey) { "add" { Process-Add $prop.Value } "remove" { Process-Remove $prop.Value } "modify" { Process-Modify $prop.Value } } } # ============================================================ # Section 14: Save + validate # ============================================================ # Save XML $settings = New-Object System.Xml.XmlWriterSettings $settings.Encoding = New-Object System.Text.UTF8Encoding($true) # with BOM $settings.Indent = $false # preserve original whitespace $settings.NewLineHandling = [System.Xml.NewLineHandling]::None # Write using XmlWriter to get proper encoding declaration $memStream = New-Object System.IO.MemoryStream $writer = [System.Xml.XmlWriter]::Create($memStream, $settings) $script:xmlDoc.Save($writer) $writer.Flush() $writer.Close() $bytes = $memStream.ToArray() $memStream.Close() # Fix encoding case: utf-8 → UTF-8 (cosmetic, 1C accepts both) $text = [System.Text.Encoding]::UTF8.GetString($bytes) # Remove BOM from string if present (we'll add it as bytes) if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } $text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') # Write with BOM $utf8Bom = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) Info "Saved: $resolvedPath" # ============================================================ # Section 15: Auto-validate # ============================================================ if (-not $NoValidate) { $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\meta-validate") "scripts\meta-validate.ps1" $validateScript = [System.IO.Path]::GetFullPath($validateScript) if (Test-Path $validateScript) { Write-Host "" Write-Host "--- Running meta-validate ---" -ForegroundColor DarkGray & powershell.exe -NoProfile -File $validateScript -ObjectPath $resolvedPath } else { Write-Host "" Write-Host "[SKIP] meta-validate not found at: $validateScript" -ForegroundColor DarkGray } } # ============================================================ # Section 16: Summary # ============================================================ Write-Host "" Write-Host "=== meta-edit summary ===" -ForegroundColor Green Write-Host " Object: $($script:objType).$($script:objName)" Write-Host " Added: $($script:addCount)" Write-Host " Removed: $($script:removeCount)" Write-Host " Modified: $($script:modifyCount)" if ($script:warnCount -gt 0) { Write-Host " Warnings: $($script:warnCount)" -ForegroundColor Yellow } $totalChanges = $script:addCount + $script:removeCount + $script:modifyCount if ($totalChanges -eq 0) { Write-Host " No changes applied." -ForegroundColor Yellow } exit 0