diff --git a/.claude/skills/meta-edit/SKILL.md b/.claude/skills/meta-edit/SKILL.md index 4796b2a8..a36bd9a6 100644 --- a/.claude/skills/meta-edit/SKILL.md +++ b/.claude/skills/meta-edit/SKILL.md @@ -1,7 +1,7 @@ --- name: meta-edit -description: Точечное редактирование объекта метаданных 1С (добавление/удаление/модификация реквизитов, ТЧ, измерений, ресурсов, значений перечислений, свойств объекта) -argument-hint: [-NoValidate] +description: Точечное редактирование объекта метаданных 1С (добавление/удаление/модификация реквизитов, ТЧ, измерений, ресурсов, значений перечислений, свойств объекта, владельцев, движений, ввода по строке) +argument-hint: -Operation -Value "" | -DefinitionFile [-NoValidate] allowed-tools: - Bash - Read @@ -11,23 +11,127 @@ allowed-tools: # /meta-edit — точечное редактирование метаданных 1С -Атомарные операции модификации существующих XML объектов метаданных: добавление, удаление и модификация реквизитов, табличных частей, измерений, ресурсов, значений перечислений, свойств объекта. +Атомарные операции модификации существующих XML объектов метаданных: добавление, удаление и модификация реквизитов, табличных частей, измерений, ресурсов, значений перечислений, свойств объекта, владельцев, движений по регистрам, оснований, ввода по строке. -## Параметры и команда +## Два режима работы -| Параметр | Обязательный | Описание | -|----------|:------------:|----------| -| DefinitionFile | да | JSON-файл с операциями | -| ObjectPath | да | XML-файл или директория объекта | -| NoValidate | нет | Не запускать meta-validate после правки | +### Inline mode (рекомендуется для простых операций) + +```powershell +powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 -ObjectPath "" -Operation -Value "" +``` + +### JSON mode (для сложных/комбинированных операций) ```powershell powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 -DefinitionFile "" -ObjectPath "" ``` +| Параметр | Описание | +|----------|----------| +| ObjectPath | XML-файл или директория объекта (обязательный) | +| Operation | Inline-операция (альтернатива DefinitionFile) | +| Value | Значение для inline-операции | +| DefinitionFile | JSON-файл с операциями (альтернатива Operation) | +| NoValidate | Не запускать meta-validate после правки | + `ObjectPath` авторезолв: если указана директория — ищет `.xml` в ней. -## JSON DSL — три операции +## Inline mode — операции + +### Batch-режим + +Несколько элементов через `;;`: +``` +-Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index" +``` + +### add-attribute / add-dimension / add-resource / add-column + +Shorthand-формат: `Имя: Тип | флаги` + +```powershell +-Operation add-attribute -Value "Комментарий: Строка(200)" +-Operation add-attribute -Value "Сумма: Число(15,2) | req, index" +-Operation add-attribute -Value "Ном: CatalogRef.Номенклатура | req ;; Кол: Число(15,3)" +-Operation add-dimension -Value "Организация: CatalogRef.Организации | master, mainFilter" +``` + +Позиционная вставка: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`: +```powershell +-Operation add-attribute -Value "Склад: CatalogRef.Склады >> after Организация" +``` + +### add-ts + +Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...` + +```powershell +-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)" +``` + +### add-enumValue / add-form / add-template / add-command + +Просто имена (batch через `;;`): +```powershell +-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3" +-Operation add-form -Value "ФормаЭлемента ;; ФормаСписка" +``` + +### add-owner / add-registerRecord / add-basedOn + +Полное имя метаданных `MetaType.Name`: +```powershell +-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации" +-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров" +-Operation add-basedOn -Value "Document.ЗаказКлиента" +``` + +### add-inputByString + +Пути полей (префикс `MetaType.Name.` добавляется автоматически): +```powershell +-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" +``` + +### remove-* + +Имя элемента (или несколько через `;;`): +```powershell +-Operation remove-attribute -Value "СтарыйРеквизит ;; ЕщёОдин" +-Operation remove-owner -Value "Catalog.Контрагенты" +-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code" +``` + +### modify-attribute / modify-dimension / modify-resource / modify-enumValue / modify-column + +Формат: `ИмяЭлемента: ключ=значение, ключ=значение` + +Ключи: `name` (rename), `type`, `synonym`, `indexing`, `fillChecking`, `use` и др. + +```powershell +-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)" +-Operation modify-attribute -Value "Комментарий: indexing=Index" +``` + +### modify-property + +Формат: `Ключ=Значение` (batch через `;;`): +```powershell +-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150" +-Operation modify-property -Value "Hierarchical=true" +``` + +### set-owners / set-registerRecords / set-basedOn / set-inputByString + +Заменяют **весь список** (в отличие от add/remove): +```powershell +-Operation set-owners -Value "Catalog.Организации ;; Catalog.Контрагенты" +-Operation set-registerRecords -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров" +-Operation set-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code" +``` + +## JSON DSL ### add — добавить элементы @@ -67,7 +171,13 @@ powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 - ```json { "modify": { - "properties": { "CodeLength": 11, "Hierarchical": true }, + "properties": { + "CodeLength": 11, + "Hierarchical": true, + "Owners": ["Catalog.Контрагенты", "Catalog.Организации"], + "RegisterRecords": ["AccumulationRegister.Продажи"], + "InputByString": ["StandardAttribute.Description"] + }, "реквизиты": { "Комментарий": { "type": "Строка(500)" }, "СтароеИмя": { "name": "НовоеИмя" } @@ -78,7 +188,7 @@ powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 - ### Комбинирование -Все три операции можно указать в одном JSON-файле. +Все три операции можно указать в одном JSON-файле. Для сложных сценариев (ТЧ с реквизитами + удаление + модификация) используйте JSON DSL. ### Синонимы ключей (case-insensitive) @@ -103,14 +213,11 @@ powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 - `Строка(200)`, `Число(15,2)`, `Булево`, `Дата`, `ДатаВремя`, `ХранилищеЗначения`, `СправочникСсылка.XXX`, `ДокументСсылка.XXX`, `ПеречислениеСсылка.XXX`, `ОпределяемыйТип.XXX`. -### Позиционная вставка (опционально) +### Позиционная вставка -```json -{ "name": "Склад", "type": "CatalogRef.Склады", "after": "Организация" } -{ "name": "Приоритет", "type": "Число(1)", "before": "Комментарий" } -``` +JSON: `{ "name": "Склад", "type": "CatalogRef.Склады", "after": "Организация" }` -Без указания — в конец группы однотипных элементов. +Inline: `"Склад: CatalogRef.Склады >> after Организация"` или `"Склад: CatalogRef.Склады << before Комментарий"` ### Shorthand-формат реквизитов @@ -118,6 +225,17 @@ powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 - "ИмяРеквизита: Тип | req, index" ``` +## Complex properties — Owners, RegisterRecords, BasedOn, InputByString + +Свойства со вложенной XML-структурой (не скалярный InnerText). Поддерживаются через inline-операции `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`. + +| Свойство | Объекты | XML-тег | Inline-значение | +|----------|---------|---------|-----------------| +| Owners | Catalog, ChartOfCharacteristicTypes | `` | `Catalog.XXX` | +| RegisterRecords | Document | `` | `AccumulationRegister.XXX` | +| BasedOn | Document, Catalog, BP, Task | `` | `Document.XXX` | +| InputByString | Catalog, ChartOf*, Task | `` | `StandardAttribute.Description` | + ## Поддерживаемые типы объектов | Тип объекта | Допустимые add-типы | @@ -130,58 +248,55 @@ powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 - ## Примеры -### Добавить реквизиты в справочник +### Inline: добавить реквизиты -```json -{ - "add": { - "attributes": [ - "Комментарий: String(200)", - { "name": "Сумма", "type": "Number(15,2)", "indexing": "Index" } - ] - } -} +```powershell +-Operation add-attribute -Value "Комментарий: String(200) ;; Сумма: Число(15,2) | index" ``` -### Добавить ТЧ в документ +### Inline: добавить ТЧ с реквизитами + +```powershell +-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2)" +``` + +### Inline: удалить + изменить (два вызова) + +```powershell +-Operation remove-attribute -Value "УстаревшийРеквизит" +-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=String(500)" +``` + +### Inline: владельцы справочника + +```powershell +-Operation set-owners -Value "Catalog.Контрагенты ;; Catalog.Организации" +``` + +### Inline: движения документа + +```powershell +-Operation add-registerRecord -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров" +``` + +### JSON: комплексное редактирование ```json { "add": { + "attributes": ["Комментарий: String(200)"], "tabularSections": [{ "name": "Товары", - "attrs": [ - "Номенклатура: CatalogRef.Номенклатура | req", - "Количество: Number(15,3)", - "Цена: Number(15,2)", - "Сумма: Number(15,2)" - ] + "attrs": ["Ном: CatalogRef.Ном | req", "Кол: Number(15,3)"] }] - } -} -``` - -### Добавить измерения и ресурсы в регистр - -```json -{ - "add": { - "dimensions": ["Организация: CatalogRef.Организации | master, mainFilter"], - "resources": ["Сумма: Number(15,2)"] - } -} -``` - -### Удалить + изменить - -```json -{ + }, "remove": { "attributes": ["УстаревшийРеквизит"] }, "modify": { - "properties": { "DescriptionLength": 150 }, - "attributes": { - "СтароеИмя": { "name": "НовоеИмя", "type": "String(500)" } - } + "properties": { + "DescriptionLength": 150, + "Owners": ["Catalog.Контрагенты", "Catalog.Организации"] + }, + "attributes": { "СтароеИмя": { "name": "НовоеИмя" } } } } ``` @@ -201,3 +316,7 @@ powershell.exe -NoProfile -File .claude\skills\meta-edit\scripts\meta-edit.ps1 - - **Изменение свойств** объекта (длина кода, иерархия и т.д.) - **Добавление значений** перечислений - **Добавление измерений/ресурсов** в регистры +- **Управление владельцами** справочников (Owners) +- **Управление движениями** документов (RegisterRecords) +- **Настройка ввода по строке** (InputByString) +- **Управление основаниями** (BasedOn) diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index 1ad061f0..0b7941b9 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,12 +1,27 @@ -# meta-edit v1.0 — Edit existing 1C metadata object XML +# meta-edit v1.1 — Edit existing 1C metadata object XML (inline mode + complex properties) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( - [Parameter(Mandatory)] [string]$DefinitionFile, [Parameter(Mandatory)] [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", + "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", + "modify-attribute", "modify-dimension", "modify-resource", + "modify-enumValue", "modify-column", + "modify-property", + "set-owners", "set-registerRecords", "set-basedOn", "set-inputByString" + )] + [string]$Operation, + [string]$Value, + [switch]$NoValidate ) @@ -17,13 +32,26 @@ $ErrorActionPreference = "Stop" # Section 1: Parameters + loading # ============================================================ -# --- Load JSON definition --- -if (-not (Test-Path $DefinitionFile)) { - Write-Error "Definition file not found: $DefinitionFile" +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { + Write-Error "Cannot use both -DefinitionFile and -Operation" exit 1 } -$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile -$def = $jsonText | ConvertFrom-Json +if (-not $DefinitionFile -and -not $Operation) { + Write-Error "Either -DefinitionFile or -Operation is required" + exit 1 +} + +# --- 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) { @@ -553,6 +581,14 @@ function Parse-AttributeShorthand { 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() @@ -1164,6 +1200,186 @@ function Resolve-ChildTypeKey([string]$key) { 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" + } + + 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 + } + + # 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 # ============================================================ @@ -1443,6 +1659,18 @@ function Modify-Properties($propsDef) { 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]) { @@ -1647,12 +1875,196 @@ function Process-Modify($modifyDef) { } } +# ============================================================ +# 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 } +} + +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 } + + $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) { + $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 } + + $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 # ============================================================ +# --- 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)"