From cb4b5ec935201689f0a4e63ba9023a4f1d74e56a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 14 Feb 2026 15:02:23 +0300 Subject: [PATCH] Add /meta-edit skill: point editing of 1C metadata objects New skill for adding/removing/modifying attributes, tabular sections, dimensions, resources, enum values, forms, templates, commands in existing metadata XML. Supports Russian/English DSL synonyms, positional insertion, idempotent add, auto-validation via meta-validate. Also adds missing Source comment to meta-compile.ps1. Co-Authored-By: Claude Opus 4.6 --- .../meta-compile/scripts/meta-compile.ps1 | 1 + .claude/skills/meta-edit/SKILL.md | 203 ++ .../skills/meta-edit/scripts/meta-edit.ps1 | 1739 +++++++++++++++++ 3 files changed, 1943 insertions(+) create mode 100644 .claude/skills/meta-edit/SKILL.md create mode 100644 .claude/skills/meta-edit/scripts/meta-edit.ps1 diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 041d7683..daabf518 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,4 +1,5 @@ # meta-compile v1.0 — Compile 1C metadata object from JSON +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] [string]$JsonPath, diff --git a/.claude/skills/meta-edit/SKILL.md b/.claude/skills/meta-edit/SKILL.md new file mode 100644 index 00000000..4796b2a8 --- /dev/null +++ b/.claude/skills/meta-edit/SKILL.md @@ -0,0 +1,203 @@ +--- +name: meta-edit +description: Точечное редактирование объекта метаданных 1С (добавление/удаление/модификация реквизитов, ТЧ, измерений, ресурсов, значений перечислений, свойств объекта) +argument-hint: [-NoValidate] +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /meta-edit — точечное редактирование метаданных 1С + +Атомарные операции модификации существующих XML объектов метаданных: добавление, удаление и модификация реквизитов, табличных частей, измерений, ресурсов, значений перечислений, свойств объекта. + +## Параметры и команда + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| DefinitionFile | да | JSON-файл с операциями | +| ObjectPath | да | XML-файл или директория объекта | +| NoValidate | нет | Не запускать meta-validate после правки | + +```powershell +powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 -DefinitionFile "" -ObjectPath "" +``` + +`ObjectPath` авторезолв: если указана директория — ищет `.xml` в ней. + +## JSON DSL — три операции + +### add — добавить элементы + +```json +{ + "add": { + "реквизиты": [ + { "name": "Комментарий", "type": "Строка(200)" }, + { "name": "Сумма", "type": "Число(15,2)", "indexing": "Index" } + ], + "ТЧ": [{ + "name": "Товары", + "attrs": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, + { "name": "Количество", "type": "Число(15,3)" } + ] + }], + "формы": ["ФормаЭлемента"], + "макеты": ["ПечатнаяФорма"] + } +} +``` + +### remove — удалить элементы + +```json +{ + "remove": { + "реквизиты": ["СтарыйРеквизит"], + "ТЧ": ["УстаревшаяТЧ"] + } +} +``` + +### modify — изменить существующие + +```json +{ + "modify": { + "properties": { "CodeLength": 11, "Hierarchical": true }, + "реквизиты": { + "Комментарий": { "type": "Строка(500)" }, + "СтароеИмя": { "name": "НовоеИмя" } + } + } +} +``` + +### Комбинирование + +Все три операции можно указать в одном JSON-файле. + +### Синонимы ключей (case-insensitive) + +**Операции:** `add`/`добавить`, `remove`/`удалить`, `modify`/`изменить` + +**Типы дочерних элементов:** + +| Каноническое | Синонимы | +|-------------|----------| +| attributes | реквизиты, attrs | +| tabularSections | табличныеЧасти, тч, ts | +| dimensions | измерения, dims | +| resources | ресурсы, res | +| enumValues | значения, values | +| columns | графы, колонки | +| forms | формы | +| templates | макеты | +| commands | команды | +| properties | свойства | + +### Синонимы типов + +`Строка(200)`, `Число(15,2)`, `Булево`, `Дата`, `ДатаВремя`, `ХранилищеЗначения`, `СправочникСсылка.XXX`, `ДокументСсылка.XXX`, `ПеречислениеСсылка.XXX`, `ОпределяемыйТип.XXX`. + +### Позиционная вставка (опционально) + +```json +{ "name": "Склад", "type": "CatalogRef.Склады", "after": "Организация" } +{ "name": "Приоритет", "type": "Число(1)", "before": "Комментарий" } +``` + +Без указания — в конец группы однотипных элементов. + +### Shorthand-формат реквизитов + +``` +"ИмяРеквизита: Тип | req, index" +``` + +## Поддерживаемые типы объектов + +| Тип объекта | Допустимые add-типы | +|-------------|-------------------| +| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, forms, templates, commands | +| Enum | enumValues, forms, templates, commands | +| *Register (4 типа) | dimensions, resources, attributes, forms, templates, commands | +| DocumentJournal | columns, forms, templates, commands | +| Constant | forms | + +## Примеры + +### Добавить реквизиты в справочник + +```json +{ + "add": { + "attributes": [ + "Комментарий: String(200)", + { "name": "Сумма", "type": "Number(15,2)", "indexing": "Index" } + ] + } +} +``` + +### Добавить ТЧ в документ + +```json +{ + "add": { + "tabularSections": [{ + "name": "Товары", + "attrs": [ + "Номенклатура: CatalogRef.Номенклатура | req", + "Количество: Number(15,3)", + "Цена: Number(15,2)", + "Сумма: Number(15,2)" + ] + }] + } +} +``` + +### Добавить измерения и ресурсы в регистр + +```json +{ + "add": { + "dimensions": ["Организация: CatalogRef.Организации | master, mainFilter"], + "resources": ["Сумма: Number(15,2)"] + } +} +``` + +### Удалить + изменить + +```json +{ + "remove": { "attributes": ["УстаревшийРеквизит"] }, + "modify": { + "properties": { "DescriptionLength": 150 }, + "attributes": { + "СтароеИмя": { "name": "НовоеИмя", "type": "String(500)" } + } + } +} +``` + +## Верификация + +``` +/meta-validate — валидация после редактирования +/meta-info — визуальная сводка +``` + +## Когда использовать + +- **Добавление реквизитов/ТЧ** к существующим объектам конфигурации +- **Удаление устаревших** реквизитов, табличных частей +- **Переименование** реквизитов, смена типа +- **Изменение свойств** объекта (длина кода, иерархия и т.д.) +- **Добавление значений** перечислений +- **Добавление измерений/ресурсов** в регистры diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 new file mode 100644 index 00000000..1ad061f0 --- /dev/null +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -0,0 +1,1739 @@ +# meta-edit v1.0 — Edit existing 1C metadata object XML +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$DefinitionFile, + + [Parameter(Mandatory)] + [string]$ObjectPath, + + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# ============================================================ +# Section 1: Parameters + loading +# ============================================================ + +# --- Load JSON definition --- +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" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } else { + Write-Error "Directory given but no $dirName.xml found inside" + exit 1 + } +} +if (-not (Test-Path $ObjectPath)) { + Write-Error "Object file not found: $ObjectPath" + exit 1 +} +$resolvedPath = (Resolve-Path $ObjectPath).Path + +# --- 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 "" } + + $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 { "0" } + $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 + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.(.+)$') { + $sb.AppendLine("$indentcfg:$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: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 = "" + } + # 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) { "$($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" } + default { return "object" } + } +} + +function Build-AttributeFragment { + param($parsed, [string]$context, [string]$indent) + + if (-not $context) { $context = Get-AttributeContext } + $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 — for catalog/document/object contexts + if ($context -ne "register") { + $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 = $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/document only + if ($context -eq "catalog" -or $context -eq "document") { + $sb.AppendLine("$indent`t`tForItem") | Out-Null + } + + # Indexing + $indexing = "DontIndex" + if ($parsed.flags -contains "index") { $indexing = "Index" } + if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" } + if ($parsed.indexing) { $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/document only + if ($objType -in @("Catalog","Document")) { + $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.реквизиты) } + + if ($columns.Count -gt 0) { + $sb.AppendLine("$indent`t") | Out-Null + foreach ($col in $columns) { + $colParsed = Parse-AttributeShorthand $col + $sb.AppendLine($(Build-AttributeFragment $colParsed "tabular" "$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 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 + $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) { + Warn "Property '$propName' not found in Properties" + 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 + + 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++ + } + 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" } + } + $scalarEl.InnerText = $valueStr + Info "Modified $xmlTag '$elemName'.$changeProp = $valueStr" + $script:modifyCount++ + } else { + Warn "$xmlTag '$elemName': property '$changeProp' not found" + } + } + } + } + } +} + +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 13: Main processing +# ============================================================ + +$def.PSObject.Properties | ForEach-Object { + $prop = $_ + $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