# skd-edit v1.24 — Atomic 1C DCS editor # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] [Alias('Path')] [string]$TemplatePath, [Parameter(Mandatory)] [ValidateSet( "add-field","add-total","add-calculated-field","add-parameter","add-filter", "add-dataParameter","add-order","add-selection","add-dataSetLink", "add-dataSet","add-variant","add-conditionalAppearance","add-drilldown", "set-query","patch-query","set-outputParameter","set-structure", "modify-field","modify-filter","modify-dataParameter","modify-parameter","modify-structure","set-field-role", "rename-parameter","reorder-parameters", "clear-selection","clear-order","clear-filter","clear-conditionalAppearance", "remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")] [string]$Operation, [Parameter(Mandatory)] [string]$Value, [string]$DataSet, [string]$Variant, [switch]$NoSelection ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # Dirty flag — set to $true by every successful mutation. If still $false at save time, # the file is left untouched (NO-OP operations like [WARN] not found don't rewrite). $script:Dirty = $false # --- 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('>','>') } function Resolve-QueryValue { param([string]$val, [string]$baseDir) if (-not $val.StartsWith("@")) { return $val } $filePath = $val.Substring(1) if ([System.IO.Path]::IsPathRooted($filePath)) { $candidates = @($filePath) } else { $candidates = @( (Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath) ) } foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" exit 1 } $script:queryBaseDir = [System.IO.Path]::GetDirectoryName($resolvedPath) # --- 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" $script:outputParamTypes = @{ "Заголовок" = "mltext" "ВыводитьЗаголовок" = "dcsset:DataCompositionTextOutputType" "ВыводитьПараметрыДанных" = "dcsset:DataCompositionTextOutputType" "ВыводитьОтбор" = "dcsset:DataCompositionTextOutputType" "МакетОформления" = "xs:string" "РасположениеПолейГруппировки" = "dcsset:DataCompositionGroupFieldsPlacement" "РасположениеРеквизитов" = "dcsset:DataCompositionAttributesPlacement" "ГоризонтальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" "ВертикальноеРасположениеОбщихИтогов" = "dcscor:DataCompositionTotalPlacement" } 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 --- function Parse-FieldShorthand { param([string]$s) $result = @{ dataPath = ""; field = ""; title = ""; type = "" roles = @(); restrict = @() } # Extract [Title] if ($s -match '\[([^\]]+)\]') { $result.title = $Matches[1] $s = $s -replace '\s*\[[^\]]+\]', '' } # Extract @roles $roleMatches = [regex]::Matches($s, '@(\w+)') foreach ($m in $roleMatches) { $result.roles += $m.Groups[1].Value } $s = [regex]::Replace($s, '\s*@\w+', '') # Extract #restrictions $restrictMatches = [regex]::Matches($s, '#(\w+)') foreach ($m in $restrictMatches) { $result.restrict += $m.Groups[1].Value } $s = [regex]::Replace($s, '\s*#\w+', '') # Split name: type $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 Read-FieldProperties($fieldEl) { $props = @{ dataPath = ""; field = ""; title = ""; type = "" roles = @(); restrict = @() _rawTitle = $null _unknownChildren = @() } foreach ($ch in $fieldEl.ChildNodes) { if ($ch.NodeType -ne 'Element') { continue } switch ($ch.LocalName) { "dataPath" { $props.dataPath = $ch.InnerText.Trim() } "field" { $props.field = $ch.InnerText.Trim() } "title" { # Preserve full multi-lang title OuterXml — used to keep en/uk/etc. # siblings when shorthand overrides only the ru content. Strip xmlns # redeclarations that OuterXml adds for sub-elements. $raw = $ch.OuterXml $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') $props._rawTitle = $raw # Also extract ru content as plain string (backward compat — used by # external consumers reading $existing.title). foreach ($item in $ch.ChildNodes) { if ($item.NodeType -eq 'Element' -and $item.LocalName -eq 'item') { $lang = $null; $content = $null foreach ($gc in $item.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'lang') { $lang = $gc.InnerText.Trim() } if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') { $content = $gc.InnerText.Trim() } } if ($lang -eq 'ru' -and $null -ne $content) { $props.title = $content } } } } "valueType" { # Preserve the entire OuterXml so rebuild can re-emit qualifiers # (StringQualifiers, NumberQualifiers, DateQualifiers, etc.) that would # otherwise be lost. Also extract Type string for type-override shorthand. $raw = $ch.OuterXml # .NET OuterXml re-declares xmlns on every element where the prefix is in # scope (because the fragment is treated as standalone). Strip these since # the parent context at insertion point already provides them. $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') $props["_rawValueType"] = $raw $typeEl = $null foreach ($gc in $ch.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') { $typeEl = $gc; break } } if ($typeEl) { $props["_rawTypeText"] = $typeEl.InnerText.Trim() } } "role" { foreach ($gc in $ch.ChildNodes) { if ($gc.NodeType -eq 'Element') { if ($gc.LocalName -eq 'periodNumber') { $props.roles += "period" } elseif ($gc.InnerText.Trim() -eq 'true') { $props.roles += $gc.LocalName } } } } "useRestriction" { $revMap = @{ "field" = "noField"; "condition" = "noFilter"; "group" = "noGroup"; "order" = "noOrder" } foreach ($gc in $ch.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.InnerText.Trim() -eq 'true') { $mapped = $revMap[$gc.LocalName] if ($mapped) { $props.restrict += $mapped } } } } default { # Defense in depth: preserve OuterXml of unknown children so rebuild # doesn't silently drop them (custom , , etc.). $raw = $ch.OuterXml $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') $props._unknownChildren += $raw } } } return $props } function Parse-TotalShorthand { param([string]$s) # "DataPath: Func" or "DataPath: Func(expr)" or "DataPath: ИмяРесурса" (identity) $parts = $s -split ':', 2 $dataPath = $parts[0].Trim() $funcPart = $parts[1].Trim() # Known DCS aggregate functions (ru + en) $aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее', 'Sum','Count','Min','Max','Avg', 'Minimum','Maximum','Average') if ($funcPart -match '^\w+\(') { # Already has expression form: Func(expr) return @{ dataPath = $dataPath; expression = $funcPart } } elseif ($funcPart -in $aggFuncs) { # Short: Func → Func(DataPath) return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" } } else { # Identity or custom expression — use as-is return @{ dataPath = $dataPath; expression = $funcPart } } } function Parse-CalcShorthand { param([string]$s) # Pattern: "Name [Title]: type = Expression #noField #noFilter ...". # - `[Title]` is extracted only from the LHS of '=' so that `[...]` inside # an expression (e.g. index access) isn't interpreted as a title. # - `#restrict` flags use a known-names pattern and are extracted globally — # the docs put them after `=`, and the closed flag set avoids matching # `#word` that happens to appear inside a string literal. $restrictPattern = '#(noField|noFilter|noCondition|noGroup|noOrder)\b' $restrict = @() foreach ($m in [regex]::Matches($s, $restrictPattern)) { $restrict += $m.Groups[1].Value } $s = [regex]::Replace($s, "\s*$restrictPattern", '') $eqIdx = $s.IndexOf('=') if ($eqIdx -gt 0) { $lhs = $s.Substring(0, $eqIdx) $rhs = $s.Substring($eqIdx + 1).Trim() } else { $lhs = $s $rhs = $null } $title = "" if ($lhs -match '\[([^\]]+)\]') { $title = $Matches[1] $lhs = $lhs -replace '\s*\[[^\]]+\]', '' } $lhs = $lhs.Trim() if ($null -ne $rhs) { if ($lhs.Contains(':')) { $colonIdx = $lhs.IndexOf(':') $dataPath = $lhs.Substring(0, $colonIdx).Trim() $type = Resolve-TypeStr ($lhs.Substring($colonIdx + 1).Trim()) return @{ dataPath = $dataPath; expression = $rhs; type = $type; title = $title; restrict = $restrict } } return @{ dataPath = $lhs; expression = $rhs; type = ""; title = $title; restrict = $restrict } } return @{ dataPath = $lhs; expression = ""; type = ""; title = $title; restrict = $restrict } } function Parse-ParamShorthand { param([string]$s) $result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null; hidden = $false; always = $false; availableValues = @(); valueListAllowed = $false } # Extract availableValue=... (must be before main parse — captures to end of string) if ($s -match '\s*availableValue=(.+)$') { $result.availableValues = Parse-AvailableValueList $Matches[1].Trim() $s = ($s -replace '\s*availableValue=.+$', '').Trim() } if ($s -match '@autoDates') { $result.autoDates = $true $s = $s -replace '\s*@autoDates', '' } if ($s -match '@valueList\b') { $result.valueListAllowed = $true $s = $s -replace '\s*@valueList\b', '' } if ($s -match '@hidden\b') { $result.hidden = $true $s = $s -replace '\s*@hidden\b', '' } if ($s -match '@always\b') { $result.always = $true $s = $s -replace '\s*@always\b', '' } # Extract optional [Title] (mirrors Parse-FieldShorthand) if ($s -match '\[([^\]]*)\]') { $result.title = $Matches[1].Trim() $s = ($s -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } # Split "Name: Type = Value" — RHS may be empty (`= ` / `=`) → treated as empty-value sentinel if ($s -match '^([^:]+):\s*(\S+)(\s*=\s*(.*))?$') { $result.name = $Matches[1].Trim() $result.type = Resolve-TypeStr ($Matches[2].Trim()) $hasEq = $null -ne $Matches[3] $rhs = $Matches[4] if ($hasEq) { $result.value = if ($rhs) { $rhs.Trim() } else { "" } } } else { $result.name = $s.Trim() } return $result } function Parse-FilterShorthand { param([string]$s) # use is tristate: $null = not specified (modify-* won't touch), # $false = @off (explicit), $true = @on (explicit). add-* writes false only when $false. $result = @{ field = ""; op = "Equal"; value = $null; use = $null; 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 '@on\b') { $result.use = $true $s = $s -replace '\s*@on\b', '' } 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" } elseif ($valPart -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') { $result.value = $valPart $result["valueType"] = "dcscor:DesignTimeValue" } else { $result.value = $valPart $result["valueType"] = "xs:string" } } } else { $result.field = $s } return $result } function Parse-DataParamShorthand { param([string]$s) # use is tristate: $null = not specified (modify-* won't touch), # $false = @off (explicit), $true = @on (explicit). add-* writes false only when $false. $result = @{ parameter = ""; value = $null; use = $null; 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 '@on\b') { $result.use = $true $s = $s -replace '\s*@on\b', '' } if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess" $s = $s -replace '\s*@quickAccess', '' } if ($s -match '@normal') { $result.viewMode = "Normal" $s = $s -replace '\s*@normal', '' } $s = $s.Trim() if ($s -match '^([^=]+)=\s*(.*)$') { $result.parameter = $Matches[1].Trim() $valStr = $Matches[2].Trim() $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") # Empty / sentinel — record as "" so caller emits xsi:nil if ($valStr -eq "" -or $valStr -eq "_" -or $valStr.ToLowerInvariant() -eq "null") { $result.value = "" } elseif ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } else { $result.value = $valStr } } else { $result.parameter = $s } return $result } function Parse-OrderShorthand { param([string]$s) $s = $s.Trim() if ($s -eq "Auto") { return @{ field = "Auto"; direction = "" } } $parts = $s -split '\s+', 2 $field = $parts[0] $dir = "Asc" if ($parts.Count -gt 1 -and $parts[1] -match '(?i)^desc$') { $dir = "Desc" } return @{ field = $field; direction = $dir } } function Parse-DataSetLinkShorthand { param([string]$s) $result = @{ source = ""; dest = ""; sourceExpr = ""; destExpr = ""; parameter = "" } # Extract optional [param ParamName] if ($s -match '\[param\s+([^\]]+)\]') { $result.parameter = $Matches[1].Trim() $s = $s -replace '\s*\[param\s+[^\]]+\]', '' } # Pattern: "Source > Dest on FieldA = FieldB" if ($s -match '^(.+?)\s*>\s*(.+?)\s+on\s+(.+?)\s*=\s*(.+)$') { $result.source = $Matches[1].Trim() $result.dest = $Matches[2].Trim() $result.sourceExpr = $Matches[3].Trim() $result.destExpr = $Matches[4].Trim() } else { Write-Error "Invalid dataSetLink shorthand: $s. Expected: 'Source > Dest on FieldA = FieldB [param Name]'" exit 1 } return $result } function Parse-DataSetShorthand { param([string]$s) $s = $s.Trim() # "Name: QUERY" — split on first ": " only if prefix is a single word (no spaces) if ($s -match '^(\S+):\s(.+)$') { return @{ name = $Matches[1]; query = $Matches[2] } } return @{ name = ""; query = $s } } function Parse-VariantShorthand { param([string]$s) $presentation = "" if ($s -match '\[([^\]]+)\]') { $presentation = $Matches[1] $s = $s -replace '\s*\[[^\]]+\]', '' } $name = $s.Trim() if (-not $presentation) { $presentation = $name } return @{ name = $name; presentation = $presentation } } function Parse-ConditionalAppearanceShorthand { param([string]$s) $result = @{ param = ""; value = ""; filter = $null; fields = @() } # Extract " when ..." — condition part $whenIdx = $s.IndexOf(' when ') $forIdx = $s.IndexOf(' for ') # Determine boundaries $mainEnd = $s.Length if ($whenIdx -ge 0 -and $forIdx -ge 0) { $mainEnd = [Math]::Min($whenIdx, $forIdx) } elseif ($whenIdx -ge 0) { $mainEnd = $whenIdx } elseif ($forIdx -ge 0) { $mainEnd = $forIdx } # Parse "for" fields if ($forIdx -ge 0) { $forEnd = $s.Length if ($whenIdx -gt $forIdx) { $forEnd = $whenIdx } $forPart = $s.Substring($forIdx + 5, $forEnd - $forIdx - 5).Trim() $result.fields = @($forPart -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } # Parse "when" filter (supports " or " for OrGroup) if ($whenIdx -ge 0) { $whenEnd = $s.Length if ($forIdx -gt $whenIdx) { $whenEnd = $forIdx } $whenPart = $s.Substring($whenIdx + 6, $whenEnd - $whenIdx - 6).Trim() $orParts = $whenPart -split '\s+or\s+' if ($orParts.Count -gt 1) { $result.filter = @($orParts | ForEach-Object { Parse-FilterShorthand $_.Trim() }) } else { $result.filter = Parse-FilterShorthand $whenPart } } # Parse main part: "Param = Value" $mainPart = $s.Substring(0, $mainEnd).Trim() $eqIdx = $mainPart.IndexOf('=') if ($eqIdx -gt 0) { $result.param = $mainPart.Substring(0, $eqIdx).Trim() $result.value = $mainPart.Substring($eqIdx + 1).Trim() } else { $result.param = $mainPart } return $result } function Parse-StructureShorthand { param([string]$s) $segments = $s -split '\s*>\s*' $result = @() $innermost = $null for ($i = $segments.Count - 1; $i -ge 0; $i--) { $seg = $segments[$i].Trim() $group = @{ type = "group" } if ($seg -match '@name=(?:"([^"]+)"|''([^'']+)''|(\S+))') { $rawName = if ($Matches[1]) { $Matches[1] } elseif ($Matches[2]) { $Matches[2] } else { $Matches[3] } $group["name"] = $rawName.Trim() $seg = ($seg -replace '\s*@name=(?:"[^"]+"|''[^'']+''|\S+)', '').Trim() } if ($seg -match '^(?i)(details|детали)$') { $group["groupBy"] = @() } else { $fields = @($seg -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $group["groupBy"] = $fields } if ($null -ne $innermost) { $group["children"] = @($innermost) } $innermost = $group } if ($innermost) { $result += $innermost } return ,$result } function Parse-OutputParamShorthand { param([string]$s) $idx = $s.IndexOf('=') if ($idx -gt 0) { return @{ key = $s.Substring(0, $idx).Trim() value = $s.Substring($idx + 1).Trim() } } return @{ key = $s.Trim(); value = "" } } function Parse-AvailableValueList { # Returns array of @{ value=...; presentation=... } from comma-separated list. # Items can use 'single' or "double" quotes (stripped). Quoted spans preserve commas/colons. param([string]$s) $result = @() if (-not $s) { return ,$result } # Tokenize by ',' respecting quoted spans $items = @() $buf = New-Object System.Text.StringBuilder $inQuote = $null for ($i = 0; $i -lt $s.Length; $i++) { $ch = $s[$i] if ($inQuote) { [void]$buf.Append($ch) if ($ch -eq $inQuote) { $inQuote = $null } } elseif ($ch -eq "'" -or $ch -eq '"') { $inQuote = $ch [void]$buf.Append($ch) } elseif ($ch -eq ',') { $items += $buf.ToString() [void]$buf.Clear() } else { [void]$buf.Append($ch) } } if ($buf.Length -gt 0) { $items += $buf.ToString() } # For each item: split into value[:presentation], strip quotes $stripQuotes = { param($t) $t = $t.Trim() if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { return $t.Substring(1, $t.Length - 2) } return $t } foreach ($raw in $items) { $item = $raw.Trim() if (-not $item) { continue } # Find first ':' outside quotes $colonIdx = -1 $q = $null for ($j = 0; $j -lt $item.Length; $j++) { $c = $item[$j] if ($q) { if ($c -eq $q) { $q = $null } } elseif ($c -eq "'" -or $c -eq '"') { $q = $c } elseif ($c -eq ':') { $colonIdx = $j; break } } if ($colonIdx -ge 0) { $valPart = $item.Substring(0, $colonIdx) $presPart = $item.Substring($colonIdx + 1) $result += @{ value = (& $stripQuotes $valPart); presentation = (& $stripQuotes $presPart) } } else { $result += @{ value = (& $stripQuotes $item); presentation = "" } } } return ,$result } # --- 4. Build-* functions (XML fragment generators) --- function Build-ValueTypeXml { param($typeStr, [string]$indent) if (-not $typeStr) { return "" } # Composite: array of types — concatenate per-type fragments if ($typeStr -is [array] -or $typeStr -is [System.Collections.IList]) { $parts = @() foreach ($t in $typeStr) { $p = Build-ValueTypeXml -typeStr "$t" -indent $indent if ($p) { $parts += $p } } return $parts -join "`n" } $typeStr = Resolve-TypeStr "$typeStr" $lines = @() if ($typeStr -eq "boolean") { $lines += "$indentxs:boolean" return $lines -join "`n" } # string, string(N), string(N,fix) — fix → AllowedLength=Fixed if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') { $len = if ($Matches[2]) { $Matches[2] } else { "0" } $al = if ($Matches[4]) { "Fixed" } else { "Variable" } $lines += "$indentxs:string" $lines += "$indent" $lines += "$indent`t$len" $lines += "$indent`t$al" $lines += "$indent" return $lines -join "`n" } # decimal forms — bare decimal = money 10,2; decimal(N) = integer N,0 if ($typeStr -match '^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$') { if (-not $Matches[1]) { $digits = "10"; $fraction = "2"; $sign = "Any" } else { $digits = $Matches[2] $fraction = if ($Matches[4]) { $Matches[4] } else { "0" } $sign = if ($Matches[5]) { "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 "`n" } # date / dateTime / time — all xs:dateTime, differ only in DateFractions if ($typeStr -match '^(date|dateTime|time)$') { $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } "time" { "Time" } } $lines += "$indentxs:dateTime" $lines += "$indent" $lines += "$indent`t$fractions" $lines += "$indent" return $lines -join "`n" } if ($typeStr -eq "StandardPeriod") { $lines += "$indentv8:StandardPeriod" return $lines -join "`n" } if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { $lines += "$indentd5p1:$(Esc-Xml $typeStr)" return $lines -join "`n" } if ($typeStr.Contains('.')) { $lines += "$indentd5p1:$(Esc-Xml $typeStr)" return $lines -join "`n" } $lines += "$indent$(Esc-Xml $typeStr)" return $lines -join "`n" } # Sentinel-normalized empty check — null / "" / "_" / "null" (case-insensitive). function Test-EmptyValue { param($v) if ($null -eq $v) { return $true } $s = "$v".Trim() if ($s -eq "") { return $true } if ($s -eq "_") { return $true } if ($s.ToLowerInvariant() -eq "null") { return $true } return $false } # Returns XML fragment string for a type-aware empty . # Empty + valueListAllowed → omit entirely (returns $null). # tagPrefix used for dcscor: in data parameters. function Build-EmptyValueXml { param([string]$type, [string]$indent, [string]$tagPrefix = "", [string]$tagName = "value", [bool]$valueListAllowed = $false) if ($valueListAllowed) { return $null } $t = if ($null -eq $type) { "" } else { "$type" } # Strip well-known XML schema prefixes so callers can pass raw text $t = $t -replace '^xs:', '' -replace '^v8:', '' -replace '^d\d+p\d+:', '' $pf = $tagPrefix $tn = $tagName $lines = @() if ($t -eq "") { $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" } elseif ($t -eq "StandardPeriod") { $lines += "$indent<${pf}${tn} xsi:type=`"v8:StandardPeriod`">" $lines += "$indent`tCustom" $lines += "$indent`t0001-01-01T00:00:00" $lines += "$indent`t0001-01-01T00:00:00" $lines += "$indent" } elseif ($t -match '^string') { $lines += "$indent<${pf}${tn} xsi:type=`"xs:string`"/>" } elseif ($t -match '^(date|time)') { $lines += "$indent<${pf}${tn} xsi:type=`"xs:dateTime`">0001-01-01T00:00:00" } elseif ($t -match '^decimal') { $lines += "$indent<${pf}${tn} xsi:type=`"xs:decimal`">0" } elseif ($t -eq "boolean") { $lines += "$indent<${pf}${tn} xsi:type=`"xs:boolean`">false" } else { # Ref types or unknown — safe nil $lines += "$indent<${pf}${tn} xsi:nil=`"true`"/>" } return $lines -join "`n" } function Build-MLTextXml { param([string]$tag, [string]$text, [string]$indent) $lines = @() $lines += "$indent<$tag xsi:type=`"v8:LocalStringType`">" $lines += "$indent`t" $lines += "$indent`t`tru" $lines += "$indent`t`t$(Esc-Xml $text)" $lines += "$indent`t" $lines += "$indent" return $lines -join "`n" } # Patches the ru within an existing multi-lang title OuterXml, preserving # en/uk/etc. siblings. Used when modify-* operates with a title-override shorthand on a # field/parameter that already has multi-language titles (typical in ERP/БП/ЗУП). # If no ru item exists, one is prepended before the first existing item. function Patch-MLTextRu { param([string]$rawOuterXml, [string]$newRuText, [string]$indent) $escaped = Esc-Xml $newRuText $ruItemPat = '(\s*ru\s*)[^<]*(\s*)' if ([regex]::IsMatch($rawOuterXml, $ruItemPat)) { return [regex]::Replace($rawOuterXml, $ruItemPat, { param($m) $m.Groups[1].Value + $escaped + $m.Groups[2].Value }) } # No ru item — prepend one inside the title element, before first . $prep = "$indent`t`n$indent`t`tru`n$indent`t`t$escaped`n$indent`t" if ($rawOuterXml -match '') { $re = New-Object System.Text.RegularExpressions.Regex('(\s*)') return $re.Replace($rawOuterXml, "`n$prep`$1", 1) } # Empty title — inject after opening tag. $re2 = New-Object System.Text.RegularExpressions.Regex('(<(?:\w+:)?title[^>]*>)') return $re2.Replace($rawOuterXml, "`$1`n$prep`n$indent", 1) } 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 "`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 "`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)" # Title: prefer raw multi-lang title (preserves en/uk/etc.). When shorthand provides # a new ru text, patch ru content inside the raw title; otherwise emit raw as-is. # When no raw title exists, fall back to ru-only build from shorthand. if ($parsed._rawTitle) { if ($parsed.title -and $parsed.title -ne $parsed._existingTitleRu) { $lines += "$i`t" + (Patch-MLTextRu $parsed._rawTitle $parsed.title "$i`t") } else { $lines += "$i`t" + $parsed._rawTitle } } elseif ($parsed.title) { $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") } 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.rawValueType) { # Preserve original verbatim — keeps qualifiers (StringQualifiers, # NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand. $lines += "$i`t" + $parsed.rawValueType } elseif ($parsed.type) { $lines += "$i`t" $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") $lines += "$i`t" } # Defense in depth: re-emit OuterXml of unknown children (e.g. , # , custom extensions) that Read-FieldProperties captured. if ($parsed._unknownChildren) { foreach ($raw in $parsed._unknownChildren) { $lines += "$i`t" + $raw } } $lines += "$i" return $lines -join "`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 "`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)" if ($parsed.title) { $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") } if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $lines += (Build-RestrictionXml -restrict $parsed.restrict -indent "$i`t") } if ($parsed.type) { $lines += "$i`t" $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") $lines += "$i`t" } $lines += "$i" return $lines -join "`n" } function Build-ParamValueXml { # Returns array of XML lines for a ... element (or StandardPeriod block). # Selects xsi:type by declared type, then falls back to value pattern. param([string]$type, [string]$value, [string]$indent, [string]$tagName = "value", [string]$tagNs = "") $i = $indent $valStr = "$value" $open = if ($tagNs) { "$tagNs`:$tagName" } else { $tagName } $lines = @() if ($type -eq "StandardPeriod") { $lines += "$i<$open xsi:type=`"v8:StandardPeriod`">" $lines += "$i`t$(Esc-Xml $valStr)" $lines += "$i`t0001-01-01T00:00:00" $lines += "$i`t0001-01-01T00:00:00" $lines += "$i" return $lines } $xsi = $null if ($type -match '^date') { $xsi = "xs:dateTime" } elseif ($type -eq "boolean") { $xsi = "xs:boolean" } elseif ($type -match '^decimal') { $xsi = "xs:decimal" } elseif ($type -match '^string') { $xsi = "xs:string" } elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { $xsi = "dcscor:DesignTimeValue" } else { # Type unknown or empty — guess from value if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $xsi = "xs:dateTime" } elseif ($valStr -eq "true" -or $valStr -eq "false") { $xsi = "xs:boolean" } elseif ($valStr -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $xsi = "dcscor:DesignTimeValue" } else { $xsi = "xs:string" } } $lines += "$i<$open xsi:type=`"$xsi`">$(Esc-Xml $valStr)" return $lines } function Build-AvailableValueFragment { # Returns XML lines (array) for a single block. param($item, [string]$declaredType, [string]$indent) $lines = @() $lines += "$indent" if (Test-EmptyValue $item.value) { $emptyXml = Build-EmptyValueXml -type $declaredType -indent "$indent`t" -tagPrefix "" -tagName "value" -valueListAllowed $false if ($emptyXml) { $lines += $emptyXml } } else { $valueLines = Build-ParamValueXml -type $declaredType -value $item.value -indent "$indent`t" foreach ($vl in $valueLines) { $lines += $vl } } if ($item.presentation) { $lines += "$indent`t" $lines += "$indent`t`t" $lines += "$indent`t`t`tru" $lines += "$indent`t`t`t$(Esc-Xml $item.presentation)" $lines += "$indent`t`t" $lines += "$indent`t" } $lines += "$indent" return $lines } function Build-ParamFragment { param($parsed, [string]$indent) $i = $indent $fragments = @() $lines = @() $lines += "$i" $lines += "$i`t$(Esc-Xml $parsed.name)" if ($parsed.title) { $lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t") } if ($parsed.type) { $lines += "$i`t" $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") $lines += "$i`t" } $vla = [bool]$parsed.valueListAllowed if ($null -ne $parsed.value) { if (Test-EmptyValue $parsed.value) { $emptyXml = Build-EmptyValueXml -type $parsed.type -indent "$i`t" -tagPrefix "" -tagName "value" -valueListAllowed $vla if ($emptyXml) { $lines += $emptyXml } } else { $valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t" foreach ($vl in $valueLines) { $lines += $vl } } } if ($parsed.hidden) { $lines += "$i`ttrue" $lines += "$i`tfalse" } if ($vla) { $lines += "$i`ttrue" } if ($parsed.availableValues -and $parsed.availableValues.Count -gt 0) { foreach ($av in $parsed.availableValues) { $avLines = Build-AvailableValueFragment -item $av -declaredType $parsed.type -indent "$i`t" foreach ($l in $avLines) { $lines += $l } } } if ($parsed.always) { $lines += "$i`tAlways" } $lines += "$i" $fragments += ($lines -join "`n") if ($parsed.autoDates) { $paramName = $parsed.name # Canonical БСП pattern: title + valueType + value + useRestriction + expression $bLines = @() $bLines += "$i" $bLines += "$i`tДатаНачала" $bLines += (Build-MLTextXml -tag "title" -text "Начало периода" -indent "$i`t") $bLines += "$i`t" $bLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") $bLines += "$i`t" $bLines += "$i`t0001-01-01T00:00:00" $bLines += "$i`ttrue" $bLines += "$i`t$(Esc-Xml "&$paramName.ДатаНачала")" $bLines += "$i" $fragments += ($bLines -join "`n") $eLines = @() $eLines += "$i" $eLines += "$i`tДатаОкончания" $eLines += (Build-MLTextXml -tag "title" -text "Конец периода" -indent "$i`t") $eLines += "$i`t" $eLines += (Build-ValueTypeXml -typeStr "date" -indent "$i`t`t") $eLines += "$i`t" $eLines += "$i`t0001-01-01T00:00:00" $eLines += "$i`ttrue" $eLines += "$i`t$(Esc-Xml "&$paramName.ДатаОкончания")" $eLines += "$i" $fragments += ($eLines -join "`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 "`n" } function Build-SelectionItemFragment { param([string]$fieldName, [string]$indent) $i = $indent $lines = @() if ($fieldName -eq "Auto") { $lines += "$i" } elseif ($fieldName -match '^Folder\((.+)\)$') { $inner = $Matches[1] $colonIdx = $inner.IndexOf(':') if ($colonIdx -gt 0) { $title = $inner.Substring(0, $colonIdx).Trim() $items = $inner.Substring($colonIdx + 1) -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } } else { $title = "" $items = $inner -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } } $lines += "$i" if ($title) { $lines += "$i`t" $lines += "$i`t`t" $lines += "$i`t`t`tru" $lines += "$i`t`t`t$(Esc-Xml $title)" $lines += "$i`t`t" $lines += "$i`t" } foreach ($item in $items) { $lines += "$i`t" $lines += "$i`t`t$(Esc-Xml $item)" $lines += "$i`t" } $lines += "$i`tAuto" $lines += "$i" } else { $lines += "$i" $lines += "$i`t$(Esc-Xml $fieldName)" $lines += "$i" } return $lines -join "`n" } function Build-DataParamFragment { param($parsed, [string]$indent) $i = $indent $lines = @() $lines += "$i" if ($parsed.use -eq $false) { $lines += "$i`tfalse" } $lines += "$i`t$(Esc-Xml $parsed.parameter)" if ($null -ne $parsed.value) { if ($parsed.value -is [hashtable] -and $parsed.value.variant) { $lines += "$i`t" $lines += "$i`t`t$(Esc-Xml $parsed.value.variant)" $lines += "$i`t`t0001-01-01T00:00:00" $lines += "$i`t`t0001-01-01T00:00:00" $lines += "$i`t" } elseif (Test-EmptyValue $parsed.value) { $lines += "$i`t" } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { $lines += "$i`t$(Esc-Xml "$($parsed.value)")" } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { $lines += "$i`t$(Esc-Xml "$($parsed.value)")" } else { $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 "`n" } function Build-OrderItemFragment { param($parsed, [string]$indent) $i = $indent $lines = @() if ($parsed.field -eq "Auto") { $lines += "$i" } else { $lines += "$i" $lines += "$i`t$(Esc-Xml $parsed.field)" $lines += "$i`t$($parsed.direction)" $lines += "$i" } return $lines -join "`n" } function Build-DataSetLinkFragment { param($parsed, [string]$indent) $i = $indent $lines = @() $lines += "$i" $lines += "$i`t$(Esc-Xml $parsed.source)" $lines += "$i`t$(Esc-Xml $parsed.dest)" $lines += "$i`t$(Esc-Xml $parsed.sourceExpr)" $lines += "$i`t$(Esc-Xml $parsed.destExpr)" if ($parsed.parameter) { $lines += "$i`t$(Esc-Xml $parsed.parameter)" } $lines += "$i" return $lines -join "`n" } function Build-DataSetQueryFragment { param($parsed, [string]$indent) $i = $indent $lines = @() $lines += "$i" $lines += "$i`t$(Esc-Xml $parsed.name)" $lines += "$i`t$(Esc-Xml $parsed.dataSource)" $lines += "$i`t$(Esc-Xml $parsed.query)" $lines += "$i" return $lines -join "`n" } function Build-VariantFragment { param($parsed, [string]$indent) $i = $indent $lines = @() $lines += "$i" $lines += "$i`t$(Esc-Xml $parsed.name)" $lines += (Build-MLTextXml -tag "dcsset:presentation" -text $parsed.presentation -indent "$i`t") $lines += "$i`t" $lines += "$i`t`t" $lines += "$i`t`t`t" $lines += "$i`t`t" $lines += "$i`t`t" $lines += "$i`t`t`t" $lines += "$i`t`t`t" $lines += "$i`t`t`t`t" $lines += "$i`t`t`t" $lines += "$i`t`t`t" $lines += "$i`t`t`t`t" $lines += "$i`t`t`t" $lines += "$i`t`t" $lines += "$i`t" $lines += "$i" return $lines -join "`n" } function Emit-FilterComparison { param($f, [string]$indent) $lines = @() $lines += "$indent" $lines += "$indent`t$(Esc-Xml $f.field)" $lines += "$indent`t$(Esc-Xml $f.op)" if ($null -ne $f.value) { $vt = if ($f["valueType"]) { $f["valueType"] } else { "xs:string" } $lines += "$indent`t$(Esc-Xml "$($f.value)")" } $lines += "$indent" return $lines } function Build-ConditionalAppearanceItemFragment { param($parsed, [string]$indent) $i = $indent $lines = @() $lines += "$i" # selection if ($parsed.fields -and $parsed.fields.Count -gt 0) { $lines += "$i`t" foreach ($fld in $parsed.fields) { $lines += "$i`t`t" $lines += "$i`t`t`t$(Esc-Xml $fld)" $lines += "$i`t`t" } $lines += "$i`t" } else { $lines += "$i`t" } # filter if ($parsed.filter) { $lines += "$i`t" if ($parsed.filter -is [array]) { # OrGroup $lines += "$i`t`t" $lines += "$i`t`t`tOrGroup" foreach ($f in $parsed.filter) { $lines += Emit-FilterComparison $f "$i`t`t`t" } $lines += "$i`t`t" } else { $lines += Emit-FilterComparison $parsed.filter "$i`t`t" } $lines += "$i`t" } else { $lines += "$i`t" } # appearance $lines += "$i`t" $val = $parsed.value $lines += "$i`t`t" $lines += "$i`t`t`t$(Esc-Xml $parsed.param)" if ($val -match '^(web|style|win):') { $lines += "$i`t`t`t$(Esc-Xml $val)" } elseif ($val -eq "true" -or $val -eq "false") { $lines += "$i`t`t`t$(Esc-Xml $val)" } elseif ($parsed.param -eq "Формат" -or $parsed.param -eq "Текст" -or $parsed.param -eq "Заголовок") { $lines += "$i`t`t`t" $lines += "$i`t`t`t`t" $lines += "$i`t`t`t`t`tru" $lines += "$i`t`t`t`t`t$(Esc-Xml $val)" $lines += "$i`t`t`t`t" $lines += "$i`t`t`t" } else { $lines += "$i`t`t`t$(Esc-Xml $val)" } $lines += "$i`t`t" $lines += "$i`t" $lines += "$i" return $lines -join "`n" } function Build-StructureItemFragment { param($item, [string]$indent) $i = $indent $lines = @() $lines += "$i" # name if ($item["name"]) { $lines += "$i`t$(Esc-Xml $item["name"])" } # groupItems $groupBy = $item["groupBy"] if (-not $groupBy -or $groupBy.Count -eq 0) { $lines += "$i`t" } else { $lines += "$i`t" foreach ($field in $groupBy) { $lines += "$i`t`t" $lines += "$i`t`t`t$(Esc-Xml $field)" $lines += "$i`t`t`tItems" $lines += "$i`t`t`tNone" $lines += "$i`t`t`t0001-01-01T00:00:00" $lines += "$i`t`t`t0001-01-01T00:00:00" $lines += "$i`t`t" } $lines += "$i`t" } # order (Auto) $lines += "$i`t" $lines += "$i`t`t" $lines += "$i`t" # selection (Auto) $lines += "$i`t" $lines += "$i`t`t" $lines += "$i`t" # Recursive children if ($item["children"]) { foreach ($child in $item["children"]) { $childXml = Build-StructureItemFragment -item $child -indent "$i`t" $lines += $childXml } } $lines += "$i" return $lines -join "`n" } function Build-OutputParamFragment { param($parsed, [string]$indent) $i = $indent $key = $parsed.key $val = $parsed.value $ptype = $script:outputParamTypes[$key] if (-not $ptype) { $ptype = "xs:string" } $lines = @() $lines += "$i" $lines += "$i`t$(Esc-Xml $key)" if ($ptype -eq "mltext") { $lines += "$i`t" $lines += "$i`t`t" $lines += "$i`t`t`tru" $lines += "$i`t`t`t$(Esc-Xml $val)" $lines += "$i`t`t" $lines += "$i`t" } else { $lines += "$i`t$(Esc-Xml $val)" } $lines += "$i" return $lines -join "`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) { # LF line endings — 1С DCS files use LF consistently; CRLF causes idempotency # leaks when modify-* removes one whitespace and inserts a different-style one. $ws = $xmlDoc.CreateWhitespace("`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 = $xmlDoc.CreateWhitespace("`n$parentIndent") $container.AppendChild($closeWs) | Out-Null } } } function Clear-ContainerChildren($container) { $toRemove = @() foreach ($child in $container.ChildNodes) { if ($child.NodeType -eq 'Element') { $toRemove += $child } } foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } } 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-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 Find-ElementByChildValue($container, [string]$elemName, [string]$childName, [string]$childValue, [string]$nsUri) { foreach ($child in $container.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } if ($child.LocalName -ne $elemName) { continue } if ($nsUri -and $child.NamespaceURI -ne $nsUri) { continue } foreach ($gc in $child.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq $childName -and $gc.InnerText.Trim() -eq $childValue) { return $child } } } return $null } function Set-OrCreateChildElement($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$indent) { $existing = $null foreach ($ch in $parent.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { $existing = $ch break } } if ($existing) { $existing.InnerText = $value } else { $prefix = $parent.GetPrefixOfNamespace($nsUri) $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } $fragXml = "$indent<$qualName>$(Esc-Xml $value)" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $parent $node $null $indent } } } function Set-OrCreateChildElementWithAttr($parent, [string]$localName, [string]$nsUri, [string]$value, [string]$xsiType, [string]$indent) { $existing = $null foreach ($ch in $parent.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $localName -and $ch.NamespaceURI -eq $nsUri) { $existing = $ch break } } if ($existing) { $existing.InnerText = $value if ($xsiType) { $existing.SetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance", $xsiType) | Out-Null } } else { $prefix = $parent.GetPrefixOfNamespace($nsUri) $qualName = if ($prefix) { "${prefix}:$localName" } else { $localName } $typeAttr = if ($xsiType) { " xsi:type=`"$xsiType`"" } else { "" } $fragXml = "$indent<$qualName$typeAttr>$(Esc-Xml $value)" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $parent $node $null $indent } } } function Get-AllDataSets { $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" $root = $xmlDoc.DocumentElement $result = @() foreach ($child in $root.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'dataSet' -and $child.NamespaceURI -eq $schNs) { $result += $child } } return ,$result } function Normalize-LineEndings([string]$s) { if ($null -eq $s) { return $s } return $s.Replace("`r`n", "`n").Replace("`r", "`n") } function Escape-Whitespace([string]$s) { $sb = New-Object System.Text.StringBuilder foreach ($c in $s.ToCharArray()) { $code = [int]$c if ($c -eq "`n") { [void]$sb.Append('\n') } elseif ($c -eq "`r") { [void]$sb.Append('\r') } elseif ($c -eq "`t") { [void]$sb.Append('\t') } elseif ($code -lt 32 -or $code -eq 0xA0 -or ($code -ge 0x2000 -and $code -le 0x200F) -or $code -eq 0xFEFF) { [void]$sb.AppendFormat('\u{0:X4}', $code) } else { [void]$sb.Append($c) } } return $sb.ToString() } function Collapse-Whitespace([string]$s) { return ([regex]::Replace($s, "[\s ]+", " ")).Trim() } function Find-LongestPrefixMatch([string]$haystack, [string]$needle) { # Binary search: largest L such that needle.Substring(0, L) is a substring of haystack. # Monotonic — if length L matches at position P, then length L-1 (prefix) also matches at P. if ($needle.Length -eq 0 -or $haystack.Length -eq 0) { return @{ Length = 0; Offset = -1 } } if ($haystack.IndexOf([string]$needle[0]) -lt 0) { return @{ Length = 0; Offset = -1 } } $lo = 1; $hi = $needle.Length $bestLen = 1; $bestOffset = $haystack.IndexOf([string]$needle[0]) while ($lo -le $hi) { $mid = [int](($lo + $hi) / 2) $idx = $haystack.IndexOf($needle.Substring(0, $mid)) if ($idx -ge 0) { $bestLen = $mid; $bestOffset = $idx; $lo = $mid + 1 } else { $hi = $mid - 1 } } return @{ Length = $bestLen; Offset = $bestOffset } } function Format-PatchQueryNotFound([string]$oldStr, [string]$queryText, $currentDsNode, [string]$dsName) { $schNs = "http://v8.1c.ru/8.1/data-composition-system/schema" $lines = @("Substring not found in query of dataset '$dsName'.") # Step 1 — cross-dataset probe foreach ($ds in (Get-AllDataSets)) { if ($ds -eq $currentDsNode) { continue } $q = Find-FirstElement $ds @("query") $schNs if (-not $q) { continue } $qt = Normalize-LineEndings $q.InnerText if ($qt.Contains($oldStr)) { $otherName = Get-DataSetName $ds $lines += "Found in dataset '$otherName' instead — wrong -DataSet?" return ($lines -join "`n") } } # Step 2 — tolerant probe (whitespace + NBSP collapsed) $normNeedle = Collapse-Whitespace $oldStr $normHay = Collapse-Whitespace $queryText $tolerant = ($normNeedle.Length -gt 0 -and $normHay.Contains($normNeedle)) # Step 3 — prefix divergence (used by both Step 2 reporting and standalone Step 3) $prefix = Find-LongestPrefixMatch -haystack $queryText -needle $oldStr $divergence = $null if ($prefix.Length -gt 0 -and $prefix.Length -lt $oldStr.Length) { $queryPos = $prefix.Offset + $prefix.Length $searchChar = $oldStr[$prefix.Length] $beforeLen = [Math]::Min(20, $prefix.Length) $before = $oldStr.Substring($prefix.Length - $beforeLen, $beforeLen) $divergence = [ordered]@{ matched = $prefix.Length total = $oldStr.Length before = $before searchChar = $searchChar queryChar = $(if ($queryPos -lt $queryText.Length) { $queryText[$queryPos] } else { $null }) } } if ($tolerant) { $lines += "Not found exactly, but would match with whitespace normalized (tabs/spaces/NBSP)." if ($divergence) { $lines += "Diverged at offset $($divergence.matched) of $($divergence.total):" $lines += " before: '$(Escape-Whitespace $divergence.before)'" $lines += " in search: '$(Escape-Whitespace ([string]$divergence.searchChar))' (U+$('{0:X4}' -f [int]$divergence.searchChar))" if ($null -ne $divergence.queryChar) { $lines += " in query: '$(Escape-Whitespace ([string]$divergence.queryChar))' (U+$('{0:X4}' -f [int]$divergence.queryChar))" } } return ($lines -join "`n") } # Step 3 standalone if ($prefix.Length -eq 0) { $lines += "No common prefix with query. Check -DataSet (current: '$dsName')." return ($lines -join "`n") } $lines += "Matched first $($divergence.matched) of $($divergence.total) chars, then diverged:" $lines += " before: '$(Escape-Whitespace $divergence.before)'" $lines += " in search: '$(Escape-Whitespace ([string]$divergence.searchChar))' (U+$('{0:X4}' -f [int]$divergence.searchChar))" if ($null -ne $divergence.queryChar) { $lines += " in query: '$(Escape-Whitespace ([string]$divergence.queryChar))' (U+$('{0:X4}' -f [int]$divergence.queryChar))" } else { $lines += " in query: (end of query)" } return ($lines -join "`n") } 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 } 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 $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 } } 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 Ensure-SettingsChild($settings, [string]$childName, [string[]]$afterSiblings) { $el = Find-FirstElement $settings @($childName) $setNs if ($el) { return $el } $indent = Get-ChildIndent $settings $fragXml = "$indent" $nodes = Import-Fragment $xmlDoc $fragXml $refNode = $null foreach ($sibName in $afterSiblings) { $sib = Find-FirstElement $settings @($sibName) $setNs if ($sib) { $refNode = $sib.NextSibling while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } break } } foreach ($node in $nodes) { Insert-BeforeElement $settings $node $refNode $indent } return Find-FirstElement $settings @($childName) $setNs } 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 if ($Variant) { return $Variant } 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)" } function Get-ContainerChildIndent($container) { $hasElements = $false foreach ($ch in $container.ChildNodes) { if ($ch.NodeType -eq 'Element') { $hasElements = $true; break } } if ($hasElements) { return Get-ChildIndent $container } else { $parentIndent = Get-ChildIndent $container.ParentNode return $parentIndent + "`t" } } # --- 6. Load XML --- # Capture raw original BEFORE DOM parse — needed at save time to: # (a) restore exact root opening tag (DOM serializer # collapses multi-line xmlns into a single line); # (b) detect NO-OP via byte-equality as an extra safety net. $script:RawOriginal = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) $rootOpenMatch = [regex]::Match($script:RawOriginal, ']*>') if ($rootOpenMatch.Success) { $script:RawRootOpening = $rootOpenMatch.Value } else { $script:RawRootOpening = $null } # Detect line ending convention so save can normalize back to whatever the source used. # 1С Designer writes CRLF on Windows; LF-edited files should stay LF. $script:LineEnding = if ($script:RawOriginal.Contains("`r`n")) { "`r`n" } else { "`n" } $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. Batch value splitting --- if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "modify-structure" -or $Operation -eq "add-dataSet") { $values = @($Value) } elseif ($Operation -eq "patch-query") { $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } elseif ($Operation -eq "add-drilldown") { if ($Value.Contains(';;')) { $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } else { $values = @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } } else { $values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } # --- 8. Main logic --- switch ($Operation) { "add-field" { $dsNode = Resolve-DataSet $dsName = Get-DataSetName $dsNode foreach ($val in $values) { $parsed = Parse-FieldShorthand $val $childIndent = Get-ChildIndent $dsNode # Duplicate check $existing = Find-ElementByChildValue $dsNode "field" "dataPath" $parsed.dataPath $schNs if ($existing) { Write-Host "[WARN] Field `"$($parsed.dataPath)`" already exists in dataset `"$dsName`" — skipped" continue } $fragXml = Build-FieldFragment -parsed $parsed -indent $childIndent $nodes = Import-Fragment $xmlDoc $fragXml $refNode = Find-FirstElement $dsNode @("dataSource") $schNs foreach ($node in $nodes) { Insert-BeforeElement $dsNode $node $refNode $childIndent } $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`"" if (-not $NoSelection) { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Ensure-SettingsChild $settings "selection" @() $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs if ($existingSel) { Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" } else { $selIndent = Get-ContainerChildIndent $selection $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent $selNodes = Import-Fragment $xmlDoc $selXml foreach ($node in $selNodes) { Insert-BeforeElement $selection $node $null $selIndent } $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" } } } } "add-total" { foreach ($val in $values) { $parsed = Parse-TotalShorthand $val $childIndent = Get-ChildIndent $xmlDoc.DocumentElement # Duplicate check $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "totalField" "dataPath" $parsed.dataPath $schNs if ($existing) { Write-Host "[WARN] TotalField `"$($parsed.dataPath)`" already exists — skipped" continue } $fragXml = Build-TotalFragment -parsed $parsed -indent $childIndent $nodes = Import-Fragment $xmlDoc $fragXml $root = $xmlDoc.DocumentElement $lastTotal = Find-LastElement $root "totalField" $schNs if ($lastTotal) { $refNode = $lastTotal.NextSibling 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 } $script:Dirty = $true; Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added" } } "add-calculated-field" { foreach ($val in $values) { $parsed = Parse-CalcShorthand $val $childIndent = Get-ChildIndent $xmlDoc.DocumentElement # Duplicate check $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "calculatedField" "dataPath" $parsed.dataPath $schNs if ($existing) { Write-Host "[WARN] CalculatedField `"$($parsed.dataPath)`" already exists — skipped" continue } $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 } $script:Dirty = $true; Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added" if (-not $NoSelection) { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Ensure-SettingsChild $settings "selection" @() $existingSel = Find-ElementByChildValue $selection "item" "field" $parsed.dataPath $setNs if ($existingSel) { Write-Host "[INFO] Field `"$($parsed.dataPath)`" already in selection — skipped" } else { $selIndent = Get-ContainerChildIndent $selection $selXml = Build-SelectionItemFragment -fieldName $parsed.dataPath -indent $selIndent $selNodes = Import-Fragment $xmlDoc $selXml foreach ($node in $selNodes) { Insert-BeforeElement $selection $node $null $selIndent } $script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`"" } } } } "add-parameter" { foreach ($val in $values) { $parsed = Parse-ParamShorthand $val $childIndent = Get-ChildIndent $xmlDoc.DocumentElement # Duplicate check $existing = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $parsed.name $schNs if ($existing) { Write-Host "[WARN] Parameter `"$($parsed.name)`" already exists — skipped" continue } $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 } } $script:Dirty = $true; Write-Host "[OK] Parameter `"$($parsed.name)`" added" if ($parsed.autoDates) { $script:Dirty = $true; Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added" } } } "modify-parameter" { foreach ($val in $values) { # Parse: "ParamName [Title] key=value key=value" # Extract optional [Title] first (mirrors Parse-FieldShorthand) $titleVal = $null if ($val -match '\[([^\]]*)\]') { $titleVal = $Matches[1].Trim() $val = ($val -replace '\s*\[[^\]]*\]\s*', ' ').Trim() } $parts = $val -split '\s+', 2 $paramName = $parts[0].Trim() $rest = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" } # Extract @hidden / @always flags $flagHidden = $false $flagAlways = $false if ($rest -match '@hidden\b') { $flagHidden = $true; $rest = ($rest -replace '\s*@hidden\b', '').Trim() } if ($rest -match '@always\b') { $flagAlways = $true; $rest = ($rest -replace '\s*@always\b', '').Trim() } # Find parameter element $paramEl = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $paramName $schNs if (-not $paramEl) { Write-Host "[WARN] Parameter `"$paramName`" not found — skipped" continue } $childIndent = Get-ChildIndent $paramEl # Set/replace title (must come right after , before ) if ($null -ne $titleVal) { $existingTitle = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'title') { $existingTitle = $ch; break } } # If the existing title has multiple (multi-language: ru + en + …), # patch only the ru via raw-string surgery to preserve other langs. # Otherwise rebuild as ru-only fragment. $titleFrag = $null if ($existingTitle) { $rawTitle = $existingTitle.OuterXml $rawTitle = [regex]::Replace($rawTitle, ' xmlns(?::\w+)?="[^"]*"', '') # Count occurrences — if >1, treat as multi-lang. $itemCount = ([regex]::Matches($rawTitle, '')).Count if ($itemCount -gt 1) { $titleFrag = $childIndent + (Patch-MLTextRu $rawTitle $titleVal $childIndent) } Remove-NodeWithWhitespace $existingTitle } if (-not $titleFrag) { $titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent } # Insert before first of (valueType, value, useRestriction, expression, availableAsField, ...) $titleRef = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -ne 'name') { $titleRef = $ch; break } } $titleNodes = Import-Fragment $xmlDoc $titleFrag foreach ($node in $titleNodes) { Insert-BeforeElement $paramEl $node $titleRef $childIndent } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`"" } # Separate availableValue=... from simple kv pairs $simpleRest = $rest $avPart = $null $avIdx = $rest.IndexOf('availableValue=') if ($avIdx -ge 0) { $simpleRest = $rest.Substring(0, $avIdx).Trim() $avPart = $rest.Substring($avIdx) } # Process simple key=value pairs (use, denyIncompleteValues, value, etc.) if ($simpleRest) { $kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)') foreach ($kv in $kvPairs) { $key = $kv.Groups[1].Value $value = $kv.Groups[2].Value # Namespace-aware lookup (children live in $schNs) $existing = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $key -and $ch.NamespaceURI -eq $schNs) { $existing = $ch; break } } if ($key -eq "value") { # Special-case: rebuild with correct xsi:type from $declaredType = "" $vtEl = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } } if ($vtEl) { foreach ($tnode in $vtEl.ChildNodes) { if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' break } } } # Detect valueListAllowed flag on the parameter — empty value should be omitted $vlaSet = $false foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { if ($ch.InnerText.Trim() -eq 'true') { $vlaSet = $true } break } } if (Test-EmptyValue $value) { $fragXml = Build-EmptyValueXml -type $declaredType -indent $childIndent -tagPrefix "" -tagName "value" -valueListAllowed $vlaSet } else { $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent $fragXml = $valueLines -join "`n" } $wasExisting = ($null -ne $existing) if ($existing) { # Capture position by next-element sibling, then remove existing $refNode = $existing.NextSibling while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } Remove-NodeWithWhitespace $existing } else { # Insert before useRestriction/availableValue/denyIncompleteValues/use $refNode = $null foreach ($child in $paramEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { $refNode = $child; break } } } if ($fragXml) { $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } } $verb = if ($wasExisting) { "updated" } else { "added" } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value $verb to $value" } elseif ($existing) { $existing.InnerText = $value $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key updated to $value" } else { # Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use $refNode = $null if ($key -eq "denyIncompleteValues") { foreach ($child in $paramEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'use') { $refNode = $child; break } } } $fragXml = "$childIndent<$key>$(Esc-Xml $value)" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key=$value added" } } } # Process availableValue — replace whole list with new items if ($avPart) { $avRest = ($avPart -replace '^availableValue=', '').Trim() $avItems = Parse-AvailableValueList $avRest # Detect value type: prefer declared of the parameter, else guess from value $declaredType = "" $vtEl = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break } } if ($vtEl) { foreach ($tnode in $vtEl.ChildNodes) { if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') { $declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', '' break } } } # Remove all existing elements $toRemove = @() foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableValue' -and $ch.NamespaceURI -eq $schNs) { $toRemove += $ch } } foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } # Insert each new before (denyIncompleteValues, use) $refNode = $null foreach ($child in $paramEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and ($child.LocalName -eq 'denyIncompleteValues' -or $child.LocalName -eq 'use')) { $refNode = $child; break } } foreach ($av in $avItems) { $avLines = Build-AvailableValueFragment -item $av -declaredType $declaredType -indent $childIndent $fragXml = $avLines -join "`n" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": availableValue set to $($avItems.Count) item(s)" } # Process @hidden / @always flags (idempotent) if ($flagHidden) { # useRestriction → true (insert after , before //...) $urEl = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'useRestriction' -and $ch.NamespaceURI -eq $schNs) { $urEl = $ch; break } } if ($urEl) { if ($urEl.InnerText.Trim() -ne 'true') { $urEl.InnerText = 'true' } } else { $refNode = $null foreach ($child in $paramEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('expression','availableAsField','availableValue','denyIncompleteValues','use')) { $refNode = $child; break } } $nodes = Import-Fragment $xmlDoc "$childIndenttrue" foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } } # availableAsField → false (insert after , before //) $afEl = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableAsField' -and $ch.NamespaceURI -eq $schNs) { $afEl = $ch; break } } if ($afEl) { if ($afEl.InnerText.Trim() -ne 'false') { $afEl.InnerText = 'false' } } else { $refNode = $null foreach ($child in $paramEl.ChildNodes) { if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refNode = $child; break } } $nodes = Import-Fragment $xmlDoc "$childIndentfalse" foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent } } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @hidden applied" } if ($flagAlways) { $useEl = $null foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $schNs) { $useEl = $ch; break } } if ($useEl) { if ($useEl.InnerText.Trim() -ne 'Always') { $useEl.InnerText = 'Always' } } else { $nodes = Import-Fragment $xmlDoc "$childIndentAlways" foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $null $childIndent } } $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @always applied" } } } "rename-parameter" { foreach ($val in $values) { # Shorthand: "OldName => NewName" if ($val -notmatch '^\s*(.+?)\s*=>\s*(.+?)\s*$') { Write-Host "[WARN] rename-parameter expects 'OldName => NewName', got: $val" continue } $oldName = $Matches[1].Trim() $newName = $Matches[2].Trim() if ($oldName -eq $newName) { Write-Host "[WARN] rename-parameter: old and new names are equal — skipped" continue } # 1. Rename OldName $root = $xmlDoc.DocumentElement $paramEl = Find-ElementByChildValue $root "parameter" "name" $oldName $schNs if (-not $paramEl) { Write-Host "[WARN] Parameter `"$oldName`" not found — skipped" continue } foreach ($ch in $paramEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $schNs) { $ch.InnerText = $newName break } } # 2. Update in other elements. # Regex matches "&OldName" only when followed by a non-identifier char (or end), # so "&Период" matches "&Период.ДатаНачала" but NOT "&ПериодОтчета". $escOld = [regex]::Escape($oldName) $exprRegex = "&$escOld(?=[^\w\u0400-\u04FF]|$)" $exprUpdated = 0 foreach ($ch in $root.ChildNodes) { if ($ch.NodeType -ne 'Element' -or $ch.LocalName -ne 'parameter' -or $ch.NamespaceURI -ne $schNs) { continue } foreach ($gc in $ch.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'expression' -and $gc.NamespaceURI -eq $schNs) { $oldExpr = $gc.InnerText $newExpr = [regex]::Replace($oldExpr, $exprRegex, "&$newName") if ($newExpr -ne $oldExpr) { $gc.InnerText = $newExpr $exprUpdated++ } } } } # 3. Update OldName in dataParameters of all variants. # Note: is in schNs, but and are in setNs. # IMPORTANT: don't use $variant — it collides with script parameter [string]$Variant # (PowerShell vars are case-insensitive, and the [string] type would coerce XmlNode to ""). $dpUpdated = 0 foreach ($variantNode in $root.ChildNodes) { if ($variantNode.NodeType -ne 'Element' -or $variantNode.LocalName -ne 'settingsVariant' -or $variantNode.NamespaceURI -ne $schNs) { continue } $settings = Find-FirstElement $variantNode @("settings") $setNs if (-not $settings) { continue } $dpEl = Find-FirstElement $settings @("dataParameters") $setNs if (-not $dpEl) { continue } foreach ($item in $dpEl.ChildNodes) { if ($item.NodeType -ne 'Element' -or $item.LocalName -ne 'item') { continue } foreach ($gc in $item.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'parameter' -and $gc.NamespaceURI -eq $corNs) { if ($gc.InnerText.Trim() -eq $oldName) { $gc.InnerText = $newName $dpUpdated++ } } } } } $script:Dirty = $true; Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)" } } "reorder-parameters" { foreach ($val in $values) { # Shorthand: "Name1, Name2, Name3" — partial list, listed names go first in order, rest preserve original order $order = @($val -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) if ($order.Count -eq 0) { Write-Host "[WARN] reorder-parameters: empty list — skipped" continue } $root = $xmlDoc.DocumentElement # Collect all in document order with their child indent $allParams = @() foreach ($ch in $root.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'parameter' -and $ch.NamespaceURI -eq $schNs) { $allParams += $ch } } if ($allParams.Count -eq 0) { Write-Host "[WARN] reorder-parameters: no parameters in schema" continue } $childIndent = Get-ChildIndent $root # Build name -> element map $byName = @{} foreach ($pe in $allParams) { foreach ($gc in $pe.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { $byName[$gc.InnerText.Trim()] = $pe break } } } # Build new order $newOrder = @() $used = @{} foreach ($name in $order) { if ($byName.ContainsKey($name)) { $newOrder += $byName[$name] $used[$name] = $true } else { Write-Host "[WARN] reorder-parameters: parameter `"$name`" not found — skipped" } } foreach ($pe in $allParams) { $peName = $null foreach ($gc in $pe.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $schNs) { $peName = $gc.InnerText.Trim(); break } } if ($peName -and -not $used.ContainsKey($peName)) { $newOrder += $pe } } # Find anchor: element right after the last parameter in original order $lastParam = $allParams[-1] $anchor = $lastParam.NextSibling # Remove all parameters with surrounding whitespace foreach ($pe in $allParams) { Remove-NodeWithWhitespace $pe } # Re-insert in new order before anchor foreach ($pe in $newOrder) { Insert-BeforeElement $root $pe $anchor $childIndent } $script:Dirty = $true; Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)" } } "add-filter" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-FilterShorthand $val $filterEl = Ensure-SettingsChild $settings "filter" @("selection") $filterIndent = Get-ContainerChildIndent $filterEl $fragXml = Build-FilterItemFragment -parsed $parsed -indent $filterIndent $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $filterEl $node $null $filterIndent } $script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`"" } } "add-dataParameter" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-DataParamShorthand $val $dpEl = Ensure-SettingsChild $settings "dataParameters" @("outputParameters","conditionalAppearance","order","filter","selection") $dpIndent = Get-ContainerChildIndent $dpEl $fragXml = Build-DataParamFragment -parsed $parsed -indent $dpIndent $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $dpEl $node $null $dpIndent } $script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`"" } } "add-order" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-OrderShorthand $val $orderEl = Ensure-SettingsChild $settings "order" @("filter","selection") $orderIndent = Get-ContainerChildIndent $orderEl # Duplicate check if ($parsed.field -eq "Auto") { $isDup = $false foreach ($ch in $orderEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") if ($typeAttr -and $typeAttr.Contains("OrderItemAuto")) { $isDup = $true; break } } } if ($isDup) { Write-Host "[WARN] OrderItemAuto already exists in variant `"$varName`" — skipped" continue } } else { $existingOrd = Find-ElementByChildValue $orderEl "item" "field" $parsed.field $setNs if ($existingOrd) { Write-Host "[WARN] Order `"$($parsed.field)`" already exists in variant `"$varName`" — skipped" continue } } $fragXml = Build-OrderItemFragment -parsed $parsed -indent $orderIndent $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $orderEl $node $null $orderIndent } $desc = if ($parsed.field -eq "Auto") { "Auto" } else { "$($parsed.field) $($parsed.direction)" } $script:Dirty = $true; Write-Host "[OK] Order `"$desc`" added to variant `"$varName`"" } } "add-selection" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $fieldName = $val.Trim() $groupName = $null # Extract @group=Name if ($fieldName -match '\s*@group=(\S+)') { $groupName = $Matches[1] $fieldName = ($fieldName -replace '\s*@group=\S+', '').Trim() } if ($groupName) { # Find named StructureItemGroup $dcssetNs = "http://v8.1c.ru/8.1/data-composition-system/settings" $xsiNs = "http://www.w3.org/2001/XMLSchema-instance" $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $nsMgr.AddNamespace("dcsset", $dcssetNs) $nsMgr.AddNamespace("xsi", $xsiNs) $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$groupName']", $nsMgr) if (-not $groupEl) { Write-Host "[WARN] StructureItemGroup `"$groupName`" not found — adding to variant level" $targetEl = $settings } else { $targetEl = $groupEl } } else { $targetEl = $settings } $selection = Ensure-SettingsChild $targetEl "selection" @() # Dedup: skip if SelectedItemAuto already exists if ($fieldName -eq "Auto") { $isDup = $false foreach ($ch in $selection.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item') { $typeAttr = $ch.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") if ($typeAttr -and $typeAttr.Contains("SelectedItemAuto")) { $isDup = $true; break } } } if ($isDup) { $target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" } Write-Host "[WARN] SelectedItemAuto already exists in $target — skipped" continue } } $selIndent = Get-ContainerChildIndent $selection $selXml = Build-SelectionItemFragment -fieldName $fieldName -indent $selIndent $selNodes = Import-Fragment $xmlDoc $selXml foreach ($node in $selNodes) { Insert-BeforeElement $selection $node $null $selIndent } $target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" } $script:Dirty = $true; Write-Host "[OK] Selection `"$fieldName`" added to $target" } } "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 } # InnerText setter handles XML escaping automatically $queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir $script:Dirty = $true; Write-Host "[OK] Query replaced in dataset `"$dsName`"" } "patch-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 } foreach ($val in $values) { $once = $false if ($val -match '@once\b') { $once = $true $val = ($val -replace '\s*@once\b', '').Trim() } $sepIdx = $val.IndexOf(" => ") if ($sepIdx -lt 0) { Write-Error "patch-query value must contain ' => ' separator: old => new" exit 1 } $oldStr = Normalize-LineEndings $val.Substring(0, $sepIdx) $newStr = Normalize-LineEndings $val.Substring($sepIdx + 4) $queryText = Normalize-LineEndings $queryEl.InnerText $count = ([regex]::Matches($queryText, [regex]::Escape($oldStr))).Count if ($count -eq 0) { $diag = Format-PatchQueryNotFound $oldStr $queryText $dsNode $dsName Write-Error $diag exit 1 } if ($once -and $count -ne 1) { Write-Error "@once: expected 1 occurrence of '$oldStr' in dataset '$dsName', found $count" exit 1 } $queryEl.InnerText = $queryText.Replace($oldStr, $newStr) $suffix = if ($once) { " (1 occurrence)" } else { " ($count occurrence(s))" } $script:Dirty = $true; Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix" } } "set-outputParameter" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-OutputParamShorthand $val $outputEl = Ensure-SettingsChild $settings "outputParameters" @("conditionalAppearance","order","filter","selection") $outputIndent = Get-ContainerChildIndent $outputEl # Remove existing parameter with same key if present $existingParam = Find-ElementByChildValue $outputEl "item" "parameter" $parsed.key $corNs if ($existingParam) { Remove-NodeWithWhitespace $existingParam $script:Dirty = $true; Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`"" } else { $script:Dirty = $true; Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`"" } $fragXml = Build-OutputParamFragment -parsed $parsed -indent $outputIndent $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $outputEl $node $null $outputIndent } } } "set-structure" { $settings = Resolve-VariantSettings $varName = Get-VariantName # Remove all existing structure items (dcsset:item elements) $toRemove = @() foreach ($ch in $settings.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'item' -and $ch.NamespaceURI -eq $setNs) { $toRemove += $ch } } foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } # Parse structure shorthand $structItems = Parse-StructureShorthand $Value $settingsIndent = Get-ChildIndent $settings # Find insertion point — before outputParameters/dataParameters/conditionalAppearance/order/filter/selection or at end $refNode = Find-FirstElement $settings @("outputParameters","dataParameters","conditionalAppearance","order","filter","selection","item") $setNs if (-not $refNode) { $refNode = $null } foreach ($structItem in $structItems) { $fragXml = Build-StructureItemFragment -item $structItem -indent $settingsIndent $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $settings $node $refNode $settingsIndent } } $script:Dirty = $true; Write-Host "[OK] Structure set in variant `"$varName`": $Value" } "modify-structure" { $settings = Resolve-VariantSettings $varName = Get-VariantName $structItems = Parse-StructureShorthand $Value # Flatten parsed tree into (name, groupBy) targets $targets = @() $stack = New-Object System.Collections.Stack foreach ($it in $structItems) { $stack.Push($it) } while ($stack.Count -gt 0) { $it = $stack.Pop() if ($it["name"]) { $targets += @{ name = $it["name"]; groupBy = $it["groupBy"] } } if ($it["children"]) { foreach ($ch in $it["children"]) { $stack.Push($ch) } } } if ($targets.Count -eq 0) { Write-Error "modify-structure requires @name= for at least one group: $Value" exit 1 } $nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $nsMgr.AddNamespace("dcsset", $setNs) $nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") foreach ($t in $targets) { $groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$($t.name)']", $nsMgr) if (-not $groupEl) { Write-Host "[WARN] Group with @name=`"$($t.name)`" not found — skipped" continue } $giEl = $null foreach ($ch in $groupEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) { $giEl = $ch; break } } $groupIndent = Get-ChildIndent $groupEl if (-not $giEl) { # Create after , before //... $nameEl = $null $refAfterName = $null foreach ($ch in $groupEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $setNs) { $nameEl = $ch } elseif ($ch.NodeType -eq 'Element' -and $nameEl -and -not $refAfterName) { $refAfterName = $ch; break } } $giFrag = "$groupIndent" $nodes = Import-Fragment $xmlDoc $giFrag foreach ($node in $nodes) { Insert-BeforeElement $groupEl $node $refAfterName $groupIndent } # Re-find foreach ($ch in $groupEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) { $giEl = $ch; break } } } $toRemove = @() foreach ($ch in $giEl.ChildNodes) { if ($ch.NodeType -eq 'Element') { $toRemove += $ch } } foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el } $itemIndent = "$groupIndent`t" foreach ($field in $t.groupBy) { $lines = @() $lines += "$itemIndent" $lines += "$itemIndent`t$(Esc-Xml $field)" $lines += "$itemIndent`tItems" $lines += "$itemIndent`tNone" $lines += "$itemIndent`t0001-01-01T00:00:00" $lines += "$itemIndent`t0001-01-01T00:00:00" $lines += "$itemIndent" $fragXml = $lines -join "`n" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $giEl $node $null $itemIndent } } $desc = if ($t.groupBy.Count -eq 0) { "details" } else { $t.groupBy -join ', ' } $script:Dirty = $true; Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc" } } "add-dataSetLink" { foreach ($val in $values) { $parsed = Parse-DataSetLinkShorthand $val $root = $xmlDoc.DocumentElement $childIndent = Get-ChildIndent $root $fragXml = Build-DataSetLinkFragment -parsed $parsed -indent $childIndent $nodes = Import-Fragment $xmlDoc $fragXml # Insert after last dataSetLink, or before calculatedField/totalField/parameter/... $lastLink = Find-LastElement $root "dataSetLink" $schNs if ($lastLink) { $refNode = $lastLink.NextSibling while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } } else { $refNode = Find-FirstElement $root @("calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs } foreach ($node in $nodes) { Insert-BeforeElement $root $node $refNode $childIndent } $desc = "$($parsed.source) > $($parsed.dest) on $($parsed.sourceExpr) = $($parsed.destExpr)" if ($parsed.parameter) { $desc += " [param $($parsed.parameter)]" } $script:Dirty = $true; Write-Host "[OK] DataSetLink `"$desc`" added" } } "add-dataSet" { $root = $xmlDoc.DocumentElement $childIndent = Get-ChildIndent $root $parsed = Parse-DataSetShorthand $Value $parsed.query = Resolve-QueryValue $parsed.query $script:queryBaseDir # Auto-name if empty if (-not $parsed.name) { $count = 0 foreach ($ch in $root.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'dataSet' -and $ch.NamespaceURI -eq $schNs) { $count++ } } $parsed.name = "НаборДанных$($count + 1)" } # Duplicate check $existing = Find-ElementByChildValue $root "dataSet" "name" $parsed.name $schNs if ($existing) { Write-Host "[WARN] DataSet `"$($parsed.name)`" already exists — skipped" } else { # Get dataSource name from first existing $dsSourceEl = Find-FirstElement $root @("dataSource") $schNs $dsSourceName = "ИсточникДанных1" if ($dsSourceEl) { $nameEl = Find-FirstElement $dsSourceEl @("name") $schNs if ($nameEl) { $dsSourceName = $nameEl.InnerText.Trim() } } $parsed["dataSource"] = $dsSourceName $fragXml = Build-DataSetQueryFragment -parsed $parsed -indent $childIndent $nodes = Import-Fragment $xmlDoc $fragXml # Insert after last , or after if none $lastDS = Find-LastElement $root "dataSet" $schNs if ($lastDS) { $refNode = $lastDS.NextSibling while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } } else { $refNode = Find-FirstElement $root @("dataSetLink","calculatedField","totalField","parameter","template","groupTemplate","settingsVariant") $schNs } foreach ($node in $nodes) { Insert-BeforeElement $root $node $refNode $childIndent } $script:Dirty = $true; Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)" } } "add-variant" { $root = $xmlDoc.DocumentElement $childIndent = Get-ChildIndent $root foreach ($val in $values) { $parsed = Parse-VariantShorthand $val # Duplicate check — search for settingsVariant with matching dcsset:name $isDup = $false foreach ($ch in $root.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'settingsVariant' -and $ch.NamespaceURI -eq $schNs) { foreach ($gc in $ch.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'name' -and $gc.NamespaceURI -eq $setNs -and $gc.InnerText -eq $parsed.name) { $isDup = $true; break } } if ($isDup) { break } } } if ($isDup) { Write-Host "[WARN] Variant `"$($parsed.name)`" already exists — skipped" continue } $fragXml = Build-VariantFragment -parsed $parsed -indent $childIndent $nodes = Import-Fragment $xmlDoc $fragXml # Insert after last $lastSV = Find-LastElement $root "settingsVariant" $schNs if ($lastSV) { $refNode = $lastSV.NextSibling while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling } } else { $refNode = $null } foreach ($node in $nodes) { Insert-BeforeElement $root $node $refNode $childIndent } $script:Dirty = $true; Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added" } } "add-conditionalAppearance" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-ConditionalAppearanceShorthand $val $caEl = Ensure-SettingsChild $settings "conditionalAppearance" @("outputParameters","order","filter","selection") $caIndent = Get-ContainerChildIndent $caEl $fragXml = Build-ConditionalAppearanceItemFragment -parsed $parsed -indent $caIndent $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $caEl $node $null $caIndent } $desc = "$($parsed.param) = $($parsed.value)" if ($parsed.filter) { $desc += " when $($parsed.filter.field) $($parsed.filter.op)" } if ($parsed.fields -and $parsed.fields.Count -gt 0) { $desc += " for $($parsed.fields -join ', ')" } $script:Dirty = $true; Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`"" } } "clear-selection" { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Find-FirstElement $settings @("selection") $setNs if ($selection) { Clear-ContainerChildren $selection $script:Dirty = $true; Write-Host "[OK] Selection cleared in variant `"$varName`"" } else { Write-Host "[INFO] No selection section in variant `"$varName`"" } } "clear-order" { $settings = Resolve-VariantSettings $varName = Get-VariantName $orderEl = Find-FirstElement $settings @("order") $setNs if ($orderEl) { Clear-ContainerChildren $orderEl $script:Dirty = $true; Write-Host "[OK] Order cleared in variant `"$varName`"" } else { Write-Host "[INFO] No order section in variant `"$varName`"" } } "clear-filter" { $settings = Resolve-VariantSettings $varName = Get-VariantName $filterEl = Find-FirstElement $settings @("filter") $setNs if ($filterEl) { Clear-ContainerChildren $filterEl $script:Dirty = $true; Write-Host "[OK] Filter cleared in variant `"$varName`"" } else { Write-Host "[INFO] No filter section in variant `"$varName`"" } } "clear-conditionalAppearance" { $settings = Resolve-VariantSettings $varName = Get-VariantName $caEl = Find-FirstElement $settings @("conditionalAppearance") $setNs if ($caEl) { Clear-ContainerChildren $caEl $script:Dirty = $true; Write-Host "[OK] ConditionalAppearance cleared in variant `"$varName`"" } else { Write-Host "[INFO] No conditionalAppearance section in variant `"$varName`"" } } "modify-filter" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-FilterShorthand $val $filterEl = Find-FirstElement $settings @("filter") $setNs if (-not $filterEl) { Write-Host "[WARN] No filter section in variant `"$varName`"" continue } $filterItem = Find-ElementByChildValue $filterEl "item" "left" $parsed.field $setNs if (-not $filterItem) { Write-Host "[WARN] Filter for `"$($parsed.field)`" not found in variant `"$varName`"" continue } $itemIndent = Get-ChildIndent $filterItem # Update comparisonType Set-OrCreateChildElement $filterItem "comparisonType" $setNs $parsed.op $itemIndent # Update right value if ($null -ne $parsed.value) { $vt = if ($parsed["valueType"]) { $parsed["valueType"] } else { "xs:string" } Set-OrCreateChildElementWithAttr $filterItem "right" $setNs "$($parsed.value)" $vt $itemIndent } # Update use (only when explicitly set via @off / @on) if ($parsed.use -eq $false) { Set-OrCreateChildElement $filterItem "use" $setNs "false" $itemIndent } elseif ($parsed.use -eq $true) { # @on: remove existing use=false if any $useEl = $null foreach ($ch in $filterItem.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $setNs) { $useEl = $ch; break } } if ($useEl -and $useEl.InnerText -eq 'false') { Remove-NodeWithWhitespace $useEl } } # Update viewMode if ($parsed.viewMode) { Set-OrCreateChildElement $filterItem "viewMode" $setNs $parsed.viewMode $itemIndent } # Update userSettingID if ($parsed.userSettingID) { $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } Set-OrCreateChildElement $filterItem "userSettingID" $setNs $uid $itemIndent } $script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`"" } } "modify-dataParameter" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $parsed = Parse-DataParamShorthand $val $dpEl = Find-FirstElement $settings @("dataParameters") $setNs if (-not $dpEl) { Write-Host "[WARN] No dataParameters section in variant `"$varName`"" continue } $dpItem = Find-ElementByChildValue $dpEl "item" "parameter" $parsed.parameter $corNs if (-not $dpItem) { Write-Host "[WARN] DataParameter `"$($parsed.parameter)`" not found in variant `"$varName`"" continue } $itemIndent = Get-ChildIndent $dpItem # Update value if ($null -ne $parsed.value) { # Remove existing value element first $existingVal = $null foreach ($ch in $dpItem.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $corNs) { $existingVal = $ch; break } } if ($existingVal) { Remove-NodeWithWhitespace $existingVal } # Build new value fragment $valLines = @() if ($parsed.value -is [hashtable] -and $parsed.value.variant) { $valLines += "$itemIndent" $valLines += "$itemIndent`t$(Esc-Xml $parsed.value.variant)" $valLines += "$itemIndent`t0001-01-01T00:00:00" $valLines += "$itemIndent`t0001-01-01T00:00:00" $valLines += "$itemIndent" } elseif (Test-EmptyValue $parsed.value) { $valLines += "$itemIndent" } elseif ("$($parsed.value)" -match '^\d{4}-\d{2}-\d{2}T') { $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" } elseif ("$($parsed.value)" -eq "true" -or "$($parsed.value)" -eq "false") { $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" } else { $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" } $valXml = $valLines -join "`n" $valNodes = Import-Fragment $xmlDoc $valXml foreach ($node in $valNodes) { Insert-BeforeElement $dpItem $node $null $itemIndent } } # Update use (only when explicitly set via @off / @on) if ($parsed.use -eq $false) { Set-OrCreateChildElement $dpItem "use" $corNs "false" $itemIndent } elseif ($parsed.use -eq $true) { # @on: remove existing use=false if any $useEl = $null foreach ($ch in $dpItem.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $corNs) { $useEl = $ch; break } } if ($useEl -and $useEl.InnerText -eq 'false') { Remove-NodeWithWhitespace $useEl } } # Update viewMode if ($parsed.viewMode) { Set-OrCreateChildElement $dpItem "viewMode" $setNs $parsed.viewMode $itemIndent } # Update userSettingID if ($parsed.userSettingID) { $uid = if ($parsed.userSettingID -eq "auto") { [System.Guid]::NewGuid().ToString() } else { $parsed.userSettingID } Set-OrCreateChildElement $dpItem "userSettingID" $setNs $uid $itemIndent } $script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`"" } } "modify-field" { $dsNode = Resolve-DataSet $dsName = Get-DataSetName $dsNode foreach ($val in $values) { $parsed = Parse-FieldShorthand $val $fieldName = $parsed.dataPath # Find existing field $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs if (-not $fieldEl) { Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" continue } # Read existing properties $existing = Read-FieldProperties $fieldEl # Merge: parsed overrides existing for non-empty values $merged = @{ dataPath = $existing.dataPath field = $existing.field title = if ($parsed.title) { $parsed.title } else { $existing.title } type = if ($parsed.type) { $parsed.type } else { $existing.type } roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles } restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict } # Preserve raw only when user did NOT override type via shorthand — # otherwise the override path rebuilds valueType from $parsed.type. rawValueType = if ($parsed.type) { $null } else { $existing._rawValueType } # Preserve raw multi-lang title; pass existing ru content for change detection. _rawTitle = $existing._rawTitle _existingTitleRu = $existing.title # Pass-through unknown children (e.g. , , custom extensions). _unknownChildren = $existing._unknownChildren } # Remember position (NextSibling after whitespace) $nextSib = $fieldEl.NextSibling while ($nextSib -and ($nextSib.NodeType -eq 'Whitespace' -or $nextSib.NodeType -eq 'SignificantWhitespace')) { $nextSib = $nextSib.NextSibling } # Remove old field $childIndent = Get-ChildIndent $dsNode Remove-NodeWithWhitespace $fieldEl # Build new field fragment with merged data $fragXml = Build-FieldFragment -parsed $merged -indent $childIndent $nodes = Import-Fragment $xmlDoc $fragXml # Insert at saved position foreach ($node in $nodes) { Insert-BeforeElement $dsNode $node $nextSib $childIndent } $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`"" } } "set-field-role" { $dsNode = Resolve-DataSet $dsName = Get-DataSetName $dsNode foreach ($val in $values) { # Parse shorthand: "dataPath [@flag ...] [kv=value ...]" $s = $val.Trim() # Extract @flags $flags = @() $flagMatches = [regex]::Matches($s, '@(\w+)') foreach ($m in $flagMatches) { $flags += $m.Groups[1].Value } $s = [regex]::Replace($s, '\s*@\w+', '').Trim() # Extract kv=value (value is non-whitespace) $kv = [ordered]@{} $kvMatches = [regex]::Matches($s, '(\w+)=(\S+)') foreach ($m in $kvMatches) { $kv[$m.Groups[1].Value] = $m.Groups[2].Value } $s = [regex]::Replace($s, '\s*\w+=\S+', '').Trim() $dataPath = $s if (-not $dataPath) { Write-Host "[WARN] set-field-role: empty dataPath in `"$val`"" continue } $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $dataPath $schNs if (-not $fieldEl) { Write-Host "[WARN] Field `"$dataPath`" not found in dataset `"$dsName`"" continue } $fieldIndent = Get-ChildIndent $fieldEl # Remove existing — but first capture OuterXml of any sub-children that # Build-RoleXml won't re-emit (e.g. , , # custom extension elements). Preserved across rebuild. $oldRole = $null foreach ($ch in $fieldEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'role' -and $ch.NamespaceURI -eq $schNs) { $oldRole = $ch; break } } $knownRoleChildren = @('periodNumber','periodType','dimension','ignoreNullsInGroups','balance','account','accountTypeExpression','additionType','addition') $preservedRoleChildren = @() if ($oldRole) { foreach ($gc in $oldRole.ChildNodes) { if ($gc.NodeType -ne 'Element') { continue } if ($knownRoleChildren -contains $gc.LocalName) { continue } # kv keys override the same-named sub-element on rebuild — don't preserve # what the user explicitly set. if ($kv.Contains($gc.LocalName)) { continue } $raw = $gc.OuterXml $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') $preservedRoleChildren += $raw } Remove-NodeWithWhitespace $oldRole } # Empty spec — remove only if ($flags.Count -eq 0 -and $kv.Count -eq 0) { $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role cleared" continue } # Build new $lines = @() $lines += "$fieldIndent" foreach ($flag in $flags) { if ($flag -eq 'period') { $lines += "$fieldIndent`t1" $lines += "$fieldIndent`tMain" } else { $lines += "$fieldIndent`ttrue" } } foreach ($k in $kv.Keys) { $lines += "$fieldIndent`t$(Esc-Xml $kv[$k])" } foreach ($raw in $preservedRoleChildren) { $lines += "$fieldIndent`t" + $raw } $lines += "$fieldIndent" $fragXml = $lines -join "`n" # Insert before , else before , else at end $refNode = $null foreach ($ch in $fieldEl.ChildNodes) { if ($ch.NodeType -eq 'Element' -and $ch.LocalName -in @('valueType','inputParameters') -and $ch.NamespaceURI -eq $schNs) { $refNode = $ch; break } } $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $fieldEl $node $refNode $fieldIndent } $desc = @() if ($flags.Count -gt 0) { $desc += ($flags | ForEach-Object { "@$_" }) -join ' ' } if ($kv.Count -gt 0) { $desc += ($kv.Keys | ForEach-Object { "$_=$($kv[$_])" }) -join ' ' } $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role set: $($desc -join ' ')" } } "remove-field" { $dsNode = Resolve-DataSet $dsName = Get-DataSetName $dsNode foreach ($val in $values) { $fieldName = $val.Trim() $fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $fieldName $schNs if (-not $fieldEl) { Write-Host "[WARN] Field `"$fieldName`" not found in dataset `"$dsName`"" continue } Remove-NodeWithWhitespace $fieldEl $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`"" # Also remove from selection in variant try { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Find-FirstElement $settings @("selection") $setNs if ($selection) { $selItem = Find-ElementByChildValue $selection "item" "field" $fieldName $setNs if ($selItem) { Remove-NodeWithWhitespace $selItem $script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`"" } } } catch { # No variant — that's fine } } } "remove-total" { foreach ($val in $values) { $dataPath = $val.Trim() $root = $xmlDoc.DocumentElement $totalEl = Find-ElementByChildValue $root "totalField" "dataPath" $dataPath $schNs if (-not $totalEl) { Write-Host "[WARN] TotalField `"$dataPath`" not found" continue } Remove-NodeWithWhitespace $totalEl $script:Dirty = $true; Write-Host "[OK] TotalField `"$dataPath`" removed" } } "remove-calculated-field" { foreach ($val in $values) { $dataPath = $val.Trim() $root = $xmlDoc.DocumentElement $calcEl = Find-ElementByChildValue $root "calculatedField" "dataPath" $dataPath $schNs if (-not $calcEl) { Write-Host "[WARN] CalculatedField `"$dataPath`" not found" continue } Remove-NodeWithWhitespace $calcEl $script:Dirty = $true; Write-Host "[OK] CalculatedField `"$dataPath`" removed" # Also remove from selection try { $settings = Resolve-VariantSettings $varName = Get-VariantName $selection = Find-FirstElement $settings @("selection") $setNs if ($selection) { $selItem = Find-ElementByChildValue $selection "item" "field" $dataPath $setNs if ($selItem) { Remove-NodeWithWhitespace $selItem $script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`"" } } } catch { } } } "remove-parameter" { foreach ($val in $values) { $paramName = $val.Trim() $root = $xmlDoc.DocumentElement $paramEl = Find-ElementByChildValue $root "parameter" "name" $paramName $schNs if (-not $paramEl) { Write-Host "[WARN] Parameter `"$paramName`" not found" continue } Remove-NodeWithWhitespace $paramEl $script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`" removed" } } "remove-filter" { $settings = Resolve-VariantSettings $varName = Get-VariantName foreach ($val in $values) { $fieldName = $val.Trim() $filterEl = Find-FirstElement $settings @("filter") $setNs if (-not $filterEl) { Write-Host "[WARN] No filter section in variant `"$varName`"" continue } $filterItem = Find-ElementByChildValue $filterEl "item" "left" $fieldName $setNs if (-not $filterItem) { Write-Host "[WARN] Filter for `"$fieldName`" not found in variant `"$varName`"" continue } Remove-NodeWithWhitespace $filterItem $script:Dirty = $true; Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`"" } } "add-drilldown" { # String-based manipulation — templates use dcsat namespace with inline xmlns $rawText = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8) $nl = "`r`n" $dcsatNsDecl = 'xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template"' # Find all outer