From 151d1d77cbaf5e5174eb8eb4090a7e09a1c4c4b5 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 11 Feb 2026 16:56:02 +0300 Subject: [PATCH] Add /skd-edit skill for atomic DCS modifications New skill for point editing of existing Template.xml: add-field, add-total, add-calculated-field, add-parameter (with @autoDates), add-filter, set-query. Uses XmlDocument+PreserveWhitespace pattern from form-edit and shorthand parsers from skd-compile. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/skd-edit/SKILL.md | 110 +++ .claude/skills/skd-edit/scripts/skd-edit.ps1 | 947 +++++++++++++++++++ 2 files changed, 1057 insertions(+) create mode 100644 .claude/skills/skd-edit/SKILL.md create mode 100644 .claude/skills/skd-edit/scripts/skd-edit.ps1 diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md new file mode 100644 index 00000000..8fd815e5 --- /dev/null +++ b/.claude/skills/skd-edit/SKILL.md @@ -0,0 +1,110 @@ +--- +name: skd-edit +description: Точечное редактирование схемы компоновки данных 1С (СКД) — добавление полей, итогов, фильтров, параметров, вычисляемых полей +argument-hint: -Operation -Value +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /skd-edit — точечное редактирование СКД (Template.xml) + +Атомарные операции модификации существующей схемы компоновки данных: добавление полей, итогов, фильтров, параметров, замена запроса. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `TemplatePath` | Путь к Template.xml (или к папке — автодополнение Ext/Template.xml) | +| `Operation` | Операция: `add-field`, `add-total`, `add-calculated-field`, `add-parameter`, `add-filter`, `set-query` | +| `Value` | Значение операции (shorthand-строка или текст запроса) | +| `DataSet` | (опц.) Имя набора данных (умолч. первый) | +| `Variant` | (опц.) Имя варианта настроек (умолч. первый) | +| `NoSelection` | (опц.) Не добавлять поле в selection варианта | + +```powershell +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 -TemplatePath "" -Operation -Value "" +``` + +## Операции + +### add-field — добавить поле в набор данных + +Shorthand-формат из skd-compile: `"Цена: decimal(15,2)"`, `"Организация: CatalogRef.Организации @dimension"`. + +```powershell +-Operation add-field -Value "Цена: decimal(15,2)" +-Operation add-field -Value "Организация: CatalogRef.Организации @dimension" +-Operation add-field -Value "Служебное: string #noFilter #noOrder" +``` + +Поле добавляется перед `` в наборе, а также в `` первого варианта (если нет `-NoSelection`). + +### add-total — добавить итог + +```powershell +-Operation add-total -Value "Цена: Среднее" +-Operation add-total -Value "Стоимость: Сумма(Кол * Цена)" +``` + +### add-calculated-field — добавить вычисляемое поле + +```powershell +-Operation add-calculated-field -Value "Маржа = Продажа - Закупка" +``` + +Также добавляется в selection варианта (если нет `-NoSelection`). + +### add-parameter — добавить параметр + +```powershell +-Operation add-parameter -Value "Период: StandardPeriod = LastMonth @autoDates" +-Operation add-parameter -Value "Организация: CatalogRef.Организации" +``` + +`@autoDates` генерирует дополнительные параметры `ДатаНачала` и `ДатаОкончания`. + +### add-filter — добавить фильтр в вариант настроек + +```powershell +-Operation add-filter -Value "Номенклатура = _ @off @user" +-Operation add-filter -Value "Дата >= 2024-01-01T00:00:00" +-Operation add-filter -Value "Статус filled" +``` + +Формат: `"Поле оператор значение @флаги"`. Флаги: `@off`, `@user`, `@quickAccess`, `@normal`, `@inaccessible`. + +### set-query — заменить текст запроса + +```powershell +-Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" +``` + +## Примеры + +```powershell +# Добавить числовое поле +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-field -Value "Цена: decimal(15,2)" + +# Добавить итог +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-total -Value "Цена: Среднее" + +# Добавить фильтр +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation add-filter -Value "Организация = _ @off @user" + +# Заменить запрос +powershell.exe -NoProfile -File .claude\skills\skd-edit\scripts\skd-edit.ps1 ` + -TemplatePath test-tmp\edit-test.xml -Operation set-query -Value "ВЫБРАТЬ 1 КАК Тест" +``` + +## Верификация + +``` +/skd-validate — валидация структуры после редактирования +/skd-info — визуальная сводка +``` diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 new file mode 100644 index 00000000..fcbc811c --- /dev/null +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -0,0 +1,947 @@ +param( + [Parameter(Mandatory)] + [string]$TemplatePath, + + [Parameter(Mandatory)] + [ValidateSet("add-field","add-total","add-calculated-field", + "add-parameter","add-filter","set-query")] + [string]$Operation, + + [Parameter(Mandatory)] + [string]$Value, + + [string]$DataSet, + [string]$Variant, + [switch]$NoSelection +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Resolve path --- + +if (-not $TemplatePath.EndsWith(".xml")) { + $candidate = Join-Path (Join-Path $TemplatePath "Ext") "Template.xml" + if (Test-Path $candidate) { + $TemplatePath = $candidate + } +} + +if (-not (Test-Path $TemplatePath)) { + Write-Error "File not found: $TemplatePath" + exit 1 +} + +$resolvedPath = (Resolve-Path $TemplatePath).Path + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +# --- 2. Type system (copied from skd-compile) --- + +$script:typeSynonyms = New-Object System.Collections.Hashtable +$script:typeSynonyms["число"] = "decimal" +$script:typeSynonyms["строка"] = "string" +$script:typeSynonyms["булево"] = "boolean" +$script:typeSynonyms["дата"] = "date" +$script:typeSynonyms["датавремя"] = "dateTime" +$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod" +$script:typeSynonyms["bool"] = "boolean" +$script:typeSynonyms["str"] = "string" +$script:typeSynonyms["int"] = "decimal" +$script:typeSynonyms["integer"] = "decimal" +$script:typeSynonyms["number"] = "decimal" +$script:typeSynonyms["num"] = "decimal" +$script:typeSynonyms["справочникссылка"] = "CatalogRef" +$script:typeSynonyms["документссылка"] = "DocumentRef" +$script:typeSynonyms["перечислениессылка"] = "EnumRef" +$script:typeSynonyms["плансчетовссылка"] = "ChartOfAccountsRef" +$script:typeSynonyms["планвидовхарактеристикссылка"] = "ChartOfCharacteristicTypesRef" + +function Resolve-TypeStr { + param([string]$typeStr) + if (-not $typeStr) { return $typeStr } + + if ($typeStr -match '^([^(]+)\((.+)\)$') { + $baseName = $Matches[1].Trim() + $params = $Matches[2] + $resolved = $script:typeSynonyms[$baseName.ToLower()] + if ($resolved) { return "$resolved($params)" } + return $typeStr + } + + 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 + } + + $resolved = $script:typeSynonyms[$typeStr.ToLower()] + if ($resolved) { return $resolved } + return $typeStr +} + +# --- 3. Parsers (copied from skd-compile) --- + +function Parse-FieldShorthand { + param([string]$s) + + $result = @{ + dataPath = ""; field = ""; title = ""; type = "" + roles = @(); restrict = @() + } + + $roleMatches = [regex]::Matches($s, '@(\w+)') + foreach ($m in $roleMatches) { + $result.roles += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*@\w+', '') + + $restrictMatches = [regex]::Matches($s, '#(\w+)') + foreach ($m in $restrictMatches) { + $result.restrict += $m.Groups[1].Value + } + $s = [regex]::Replace($s, '\s*#\w+', '') + + $s = $s.Trim() + if ($s.Contains(':')) { + $parts = $s -split ':', 2 + $result.dataPath = $parts[0].Trim() + $result.type = Resolve-TypeStr ($parts[1].Trim()) + } else { + $result.dataPath = $s + } + + $result.field = $result.dataPath + return $result +} + +function Parse-TotalShorthand { + param([string]$s) + + $parts = $s -split ':', 2 + $dataPath = $parts[0].Trim() + $funcPart = $parts[1].Trim() + + if ($funcPart -match '^\w+\(') { + return @{ dataPath = $dataPath; expression = $funcPart } + } else { + return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } + } +} + +function Parse-CalcShorthand { + param([string]$s) + + $idx = $s.IndexOf('=') + if ($idx -gt 0) { + return @{ + dataPath = $s.Substring(0, $idx).Trim() + expression = $s.Substring($idx + 1).Trim() + } + } + return @{ dataPath = $s.Trim(); expression = "" } +} + +function Parse-ParamShorthand { + param([string]$s) + + $result = @{ name = ""; type = ""; value = $null; autoDates = $false } + + if ($s -match '@autoDates') { + $result.autoDates = $true + $s = $s -replace '\s*@autoDates', '' + } + + if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.+))?$') { + $result.name = $Matches[1].Trim() + $result.type = Resolve-TypeStr ($Matches[2].Trim()) + if ($Matches[4]) { + $result.value = $Matches[4].Trim() + } + } else { + $result.name = $s.Trim() + } + + return $result +} + +function Parse-FilterShorthand { + param([string]$s) + + $result = @{ field = ""; op = "Equal"; value = $null; use = $true; userSettingID = $null; viewMode = $null } + + if ($s -match '@user') { + $result.userSettingID = "auto" + $s = $s -replace '\s*@user', '' + } + if ($s -match '@off') { + $result.use = $false + $s = $s -replace '\s*@off', '' + } + if ($s -match '@quickAccess') { + $result.viewMode = "QuickAccess" + $s = $s -replace '\s*@quickAccess', '' + } + if ($s -match '@normal') { + $result.viewMode = "Normal" + $s = $s -replace '\s*@normal', '' + } + if ($s -match '@inaccessible') { + $result.viewMode = "Inaccessible" + $s = $s -replace '\s*@inaccessible', '' + } + + $s = $s.Trim() + + $opPatterns = @('<>', '>=', '<=', '=', '>', '<', + 'notIn\b', 'in\b', 'inHierarchy\b', 'inListByHierarchy\b', + 'notContains\b', 'contains\b', 'notBeginsWith\b', 'beginsWith\b', + 'notFilled\b', 'filled\b') + $opJoined = $opPatterns -join '|' + + if ($s -match "^(.+?)\s+($opJoined)\s*(.*)?$") { + $result.field = $Matches[1].Trim() + $opRaw = $Matches[2].Trim() + $valPart = if ($Matches[3]) { $Matches[3].Trim() } else { "" } + + $opMap = @{ + "=" = "Equal"; "<>" = "NotEqual"; ">" = "Greater"; ">=" = "GreaterOrEqual" + "<" = "Less"; "<=" = "LessOrEqual"; "in" = "InList"; "notIn" = "NotInList" + "inHierarchy" = "InHierarchy"; "inListByHierarchy" = "InListByHierarchy" + "contains" = "Contains"; "notContains" = "NotContains" + "beginsWith" = "BeginsWith"; "notBeginsWith" = "NotBeginsWith" + "filled" = "Filled"; "notFilled" = "NotFilled" + } + $mapped = $opMap[$opRaw] + if ($mapped) { $result.op = $mapped } else { $result.op = $opRaw } + + if ($valPart -and $valPart -ne "_") { + if ($valPart -eq "true" -or $valPart -eq "false") { + $result.value = $valPart + $result["valueType"] = "xs:boolean" + } elseif ($valPart -match '^\d{4}-\d{2}-\d{2}T') { + $result.value = $valPart + $result["valueType"] = "xs:dateTime" + } elseif ($valPart -match '^\d+(\.\d+)?$') { + $result.value = $valPart + $result["valueType"] = "xs:decimal" + } else { + $result.value = $valPart + $result["valueType"] = "xs:string" + } + } + } else { + $result.field = $s + } + + return $result +} + +# --- 4. Build-* functions (XML fragment generators) --- + +function Build-ValueTypeXml { + param([string]$typeStr, [string]$indent) + + if (-not $typeStr) { return "" } + $typeStr = Resolve-TypeStr $typeStr + $lines = @() + + if ($typeStr -eq "boolean") { + $lines += "$indentxs:boolean" + return $lines -join "`r`n" + } + + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + $lines += "$indentxs:string" + $lines += "$indent" + $lines += "$indent`t$len" + $lines += "$indent`tVariable" + $lines += "$indent" + return $lines -join "`r`n" + } + + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + $lines += "$indentxs:decimal" + $lines += "$indent" + $lines += "$indent`t$digits" + $lines += "$indent`t$fraction" + $lines += "$indent`t$sign" + $lines += "$indent" + return $lines -join "`r`n" + } + + if ($typeStr -match '^(date|dateTime)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + } + $lines += "$indentxs:dateTime" + $lines += "$indent" + $lines += "$indent`t$fractions" + $lines += "$indent" + return $lines -join "`r`n" + } + + if ($typeStr -eq "StandardPeriod") { + $lines += "$indentv8:StandardPeriod" + return $lines -join "`r`n" + } + + if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { + $lines += "$indentd5p1:$(Esc-Xml $typeStr)" + return $lines -join "`r`n" + } + + if ($typeStr.Contains('.')) { + $lines += "$indentd5p1:$(Esc-Xml $typeStr)" + return $lines -join "`r`n" + } + + $lines += "$indent$(Esc-Xml $typeStr)" + return $lines -join "`r`n" +} + +function Build-RoleXml { + param([string[]]$roles, [string]$indent) + + if (-not $roles -or $roles.Count -eq 0) { return "" } + + $lines = @() + $lines += "$indent" + foreach ($role in $roles) { + if ($role -eq "period") { + $lines += "$indent`t1" + $lines += "$indent`tMain" + } else { + $lines += "$indent`ttrue" + } + } + $lines += "$indent" + return $lines -join "`r`n" +} + +function Build-RestrictionXml { + param([string[]]$restrict, [string]$indent) + + if (-not $restrict -or $restrict.Count -eq 0) { return "" } + + $restrictMap = @{ + "noField" = "field"; "noFilter" = "condition"; "noCondition" = "condition" + "noGroup" = "group"; "noOrder" = "order" + } + + $lines = @() + $lines += "$indent" + foreach ($r in $restrict) { + $xmlName = $restrictMap["$r"] + if ($xmlName) { + $lines += "$indent`t<$xmlName>true" + } + } + $lines += "$indent" + return $lines -join "`r`n" +} + +function Build-FieldFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.dataPath)" + $lines += "$i`t$(Esc-Xml $parsed.field)" + + if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { + $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") + } + + $roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t" + if ($roleXml) { $lines += $roleXml } + + if ($parsed.type) { + $lines += "$i`t" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-TotalFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.dataPath)" + $lines += "$i`t$(Esc-Xml $parsed.expression)" + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-CalcFieldFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.dataPath)" + $lines += "$i`t$(Esc-Xml $parsed.expression)" + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-ParamFragment { + param($parsed, [string]$indent) + + $i = $indent + $fragments = @() + + # Main parameter + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $parsed.name)" + + if ($parsed.type) { + $lines += "$i`t" + $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") + $lines += "$i`t" + } + + if ($null -ne $parsed.value) { + $valStr = "$($parsed.value)" + if ($parsed.type -eq "StandardPeriod") { + $lines += "$i`t" + $lines += "$i`t`t$(Esc-Xml $valStr)" + $lines += "$i`t" + } elseif ($parsed.type -match '^date') { + $lines += "$i`t$(Esc-Xml $valStr)" + } elseif ($parsed.type -eq "boolean") { + $lines += "$i`t$(Esc-Xml $valStr)" + } elseif ($parsed.type -match '^decimal') { + $lines += "$i`t$(Esc-Xml $valStr)" + } else { + $lines += "$i`t$(Esc-Xml $valStr)" + } + } + + $lines += "$i" + $fragments += ($lines -join "`r`n") + + # @autoDates: generate ДатаНачала and ДатаОкончания + if ($parsed.autoDates) { + $paramName = $parsed.name + + $bLines = @() + $bLines += "$i" + $bLines += "$i`tДатаНачала" + $bLines += "$i`t" + $bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") + $bLines += "$i`t" + $bLines += "$i`t$(Esc-Xml "&$paramName.ДатаНачала")" + $bLines += "$i`tfalse" + $bLines += "$i" + $fragments += ($bLines -join "`r`n") + + $eLines = @() + $eLines += "$i" + $eLines += "$i`tДатаОкончания" + $eLines += "$i`t" + $eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") + $eLines += "$i`t" + $eLines += "$i`t$(Esc-Xml "&$paramName.ДатаОкончания")" + $eLines += "$i`tfalse" + $eLines += "$i" + $fragments += ($eLines -join "`r`n") + } + + return ,$fragments +} + +function Build-FilterItemFragment { + param($parsed, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + + if ($parsed.use -eq $false) { + $lines += "$i`tfalse" + } + + $lines += "$i`t$(Esc-Xml $parsed.field)" + $lines += "$i`t$(Esc-Xml $parsed.op)" + + if ($null -ne $parsed.value) { + $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } + $lines += "$i`t$(Esc-Xml "$($parsed.value)")" + } + + if ($parsed.viewMode) { + $lines += "$i`t$(Esc-Xml $parsed.viewMode)" + } + + if ($parsed.userSettingID) { + $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } + $lines += "$i`t$(Esc-Xml $uid)" + } + + $lines += "$i" + return $lines -join "`r`n" +} + +function Build-SelectionItemFragment { + param([string]$fieldName, [string]$indent) + + $i = $indent + $lines = @() + $lines += "$i" + $lines += "$i`t$(Esc-Xml $fieldName)" + $lines += "$i" + return $lines -join "`r`n" +} + +# --- 5. XML helpers --- + +function Import-Fragment($doc, [string]$xmlString) { + $wrapper = @" +<_W xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:v8="http://v8.1c.ru/8.1/data/core" + xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" + xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" + xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" + xmlns:v8ui="http://v8.1c.ru/8.1/data/ui">$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 += $doc.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] } + } + } + $depth = 0 + $current = $container + while ($current -and $current -ne $xmlDoc.DocumentElement) { + $depth++ + $current = $current.ParentNode + } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + # Append at end: insert before trailing whitespace + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $ws) | 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 = $xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Find-FirstElement($container, [string[]]$localNames, [string]$nsUri) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element') { + foreach ($name in $localNames) { + if ($child.LocalName -eq $name) { + if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { + return $child + } + } + } + } + } + return $null +} + +function Find-LastElement($container, [string]$localName, [string]$nsUri) { + $last = $null + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $localName) { + if (-not $nsUri -or $child.NamespaceURI -eq $nsUri) { + $last = $child + } + } + } + return $last +} + +function Resolve-DataSet { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $root = $xmlDoc.DocumentElement + + if ($DataSet) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $DataSet) { + return $child + } + } + } + Write-Error "DataSet '$DataSet' not found" + exit 1 + } + + # First dataset + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { + return $child + } + } + Write-Error "No dataSet found in DCS" + exit 1 +} + +function Resolve-VariantSettings { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $root = $xmlDoc.DocumentElement + + # Find settingsVariant + $sv = $null + if ($Variant) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $Variant) { + $sv = $child + break + } + } + } + if (-not $sv) { + Write-Error "Variant '$Variant' not found" + exit 1 + } + } else { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $sv = $child + break + } + } + if (-not $sv) { + Write-Error "No settingsVariant found in DCS" + exit 1 + } + } + + # Find settings element inside variant + foreach ($gc in $sv.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'settings' -and $gc.NamespaceURI -eq $setNs) { + return $gc + } + } + + Write-Error "No found in variant" + exit 1 +} + +function Get-VariantName { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + $setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" + $root = $xmlDoc.DocumentElement + + $sv = $null + if ($Variant) { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + $nameEl = $null + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + $nameEl = $gc + break + } + } + if ($nameEl -and $nameEl.InnerText -eq $Variant) { + return $Variant + } + } + } + return $Variant + } else { + foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'settingsVariant' -and $child.NamespaceURI -eq $schNs) { + foreach ($gc in $child.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs) { + return $gc.InnerText + } + } + } + } + } + return "(unknown)" +} + +function Get-DataSetName($dsNode) { + $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + foreach ($gc in $dsNode.ChildNodes) { + if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { + return $gc.InnerText + } + } + return "(unknown)" +} + +# --- 6. Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +$xmlDoc.Load($resolvedPath) + +$schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" +$setNs = "http://v8.1c.ru/8.1/data-composition-system/settings" +$corNs = "http://v8.1c.ru/8.1/data-composition-system/core" + +# --- 7. Main logic --- + +switch ($Operation) { + "add-field" { + $parsed = Parse-FieldShorthand $Value + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + $childIndent = Get-ChildIndent $dsNode + + $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert before in dataset + $refNode = Find-FirstElement $dsNode @("dataSource") $schNs + foreach ($node in $nodes) { + Insert-BeforeElement $dsNode $node $refNode $childIndent + } + + Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`"" + + # Add to selection of variant + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selNs = $setNs + $selection = Find-FirstElement $settings @("selection") $selNs + if ($selection) { + $selIndent = Get-ChildIndent $selection + $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" + } + } + } + + "add-total" { + $parsed = Parse-TotalShorthand $Value + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + # Insert after last totalField, or before parameter/template/groupTemplate/settingsVariant + $root = $xmlDoc.DocumentElement + $lastTotal = Find-LastElement $root "totalField" $schNs + if ($lastTotal) { + # Insert after last totalField = before next sibling + $refNode = $lastTotal.NextSibling + # Skip whitespace nodes + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added" + } + + "add-calculated-field" { + $parsed = Parse-CalcShorthand $Value + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + $fragXml = Build-CalcFieldFragment -parsed $parsed -indent $childIndent + $nodes = Import-Fragment $xmlDoc $fragXml + + $root = $xmlDoc.DocumentElement + $lastCalc = Find-LastElement $root "calculatedField" $schNs + if ($lastCalc) { + $refNode = $lastCalc.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("totalField","parameter","template","groupTemplate","settingsVariant") $schNs + } + + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + + Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added" + + # Add to selection + if (-not $NoSelection) { + $settings = Resolve-VariantSettings + $varName = Get-VariantName + $selection = Find-FirstElement $settings @("selection") $setNs + if ($selection) { + $selIndent = Get-ChildIndent $selection + $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent + $selNodes = Import-Fragment $xmlDoc $selXml + foreach ($node in $selNodes) { + Insert-BeforeElement $selection $node $null $selIndent + } + Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" + } + } + } + + "add-parameter" { + $parsed = Parse-ParamShorthand $Value + $childIndent = Get-ChildIndent $xmlDoc.DocumentElement + + $fragments = Build-ParamFragment -parsed $parsed -indent $childIndent + + $root = $xmlDoc.DocumentElement + $lastParam = Find-LastElement $root "parameter" $schNs + if ($lastParam) { + $refNode = $lastParam.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } else { + $refNode = Find-FirstElement $root @("template","groupTemplate","settingsVariant") $schNs + } + + foreach ($fragXml in $fragments) { + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $root $node $refNode $childIndent + } + } + + Write-Host "[OK] Parameter `"$($parsed.name)`" added" + if ($parsed.autoDates) { + Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added" + } + } + + "add-filter" { + $parsed = Parse-FilterShorthand $Value + $settings = Resolve-VariantSettings + $varName = Get-VariantName + + # Find or create + $filterEl = Find-FirstElement $settings @("filter") $setNs + if (-not $filterEl) { + # Create in settings + $settingsIndent = Get-ChildIndent $settings + $filterFragXml = "$settingsIndent" + $filterNodes = Import-Fragment $xmlDoc $filterFragXml + # Insert after selection or as first child + $refNode = Find-FirstElement $settings @("selection") $setNs + if ($refNode) { + $refNode = $refNode.NextSibling + while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { + $refNode = $refNode.NextSibling + } + } + foreach ($node in $filterNodes) { + Insert-BeforeElement $settings $node $refNode $settingsIndent + } + $filterEl = Find-FirstElement $settings @("filter") $setNs + } + + $filterIndent = Get-ChildIndent $filterEl + # If filter is self-closing (empty), we need a different indent + if (-not $filterEl.HasChildNodes) { + # Determine indent from settings indent + $settingsIndent = Get-ChildIndent $settings + $filterIndent = $settingsIndent + "`t" + } + + $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent + $nodes = Import-Fragment $xmlDoc $fragXml + foreach ($node in $nodes) { + Insert-BeforeElement $filterEl $node $null $filterIndent + } + + Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`"" + } + + "set-query" { + $dsNode = Resolve-DataSet + $dsName = Get-DataSetName $dsNode + + $queryEl = Find-FirstElement $dsNode @("query") $schNs + if (-not $queryEl) { + Write-Error "No element found in dataset '$dsName'" + exit 1 + } + + $queryEl.InnerText = Esc-Xml $Value + + Write-Host "[OK] Query replaced in dataset `"$dsName`"" + } +} + +# --- 8. Save --- + +$content = $xmlDoc.OuterXml +$content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8' +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $content, $enc) + +Write-Host "[OK] Saved $resolvedPath"