feat(skills): порядок объектов метаданных в ChildObjects

Навыки-создатели дописывали новый объект в конец группы своего вида, а стандарт
требует порядка по имени (АПК:1108). Замеры на 8 боевых выгрузках: 103 385 объектов
лежат по алфавиту, нарушают его ровно дописки в хвост. Стенд подтвердил, что
беспорядок вечен: платформа нормализует порядок ВИДОВ (возвращает канонический),
но порядок имён внутри вида не трогает.

Настройка newObjectPosition в .v8-project.json (end по умолчанию | byName) —
решение проекта, а не вызова: читают её meta-compile, role-compile, xdto-compile,
cfe-borrow и cf-edit add-childObject. Компаратор при этом константа, моделирующая
дерево Конфигуратора: ключ «ранг+символ» без культурных таблиц, одинаковый в обоих
портах на любой ОС. На корпусе он даёт 4 нарушения на 125 088 пар против 2866 у
ordinal-сравнения, которым cf-edit и cfe-borrow сортировали до сих пор.

Subsystem не упорядочивается автоматически нигде: пока подсистемы не перечислены
в <SubsystemsOrder>, порядок дерева задаёт порядок разделов в панели, а платформа
этот список сама не заводит.

Настройка не чинит накопленное, поэтому cf-edit получил операцию sort-childObjects
(вся конфигурация или названные виды). Она переставляет значения узлов, а не узлы,
поэтому диф — чистая перестановка строк. Имя вида принимается в любом регистре,
во множественном числе и по-русски; неизвестный вид — отказ со списком допустимых.

Попутно:
- PS-порты создателей переведены с DOM-сериализации на текстовую вставку: на
  выгрузке не в каноне Конфигуратора они переписывали заголовок без просьбы;
- PS-сторона семьи detect_xml_style/finalize_xml_bytes закрыта в cf-edit и
  cfe-borrow — правка чужого файла наследует его BOM/EOL/заголовок;
- xdto-compile присоединён к семье Register-InChildObjects вместо инлайн-копии;
- закрыт ложный успех в py subsystem-compile: <ChildObjects /> с пробелом ET
  разбирает, а текстовые ветки не находили — файл писался без вставки;
- cfe-borrow py не импортировал json, из-за чего резолвер молча возвращал end;
- cf-edit add-childObject ставил объект в конец блока, за пределы своей группы,
  когда видов старше в файле не было.

Проверки: 906/906 на PowerShell и 903/906 на Python, 10 гардов, верификация
эталонов платформой без падений, раундтрип отсортированной конфигурации на
8.3.24 и 8.3.27, сортировка боевой выгрузки ACC (1,4 МБ) и расширения из корпуса.

Задачу принёс PR #85; часть кода взята оттуда.

Co-Authored-By: Sergei Pleshanov <72200277+Abacadabras@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
This commit is contained in:
Nick Shirokov
2026-08-29 17:34:09 +03:00
co-authored by Sergei Pleshanov Claude Opus 5
parent 7409bacd47
commit c7b0dce151
302 changed files with 13595 additions and 388 deletions
+1
View File
@@ -34,6 +34,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -Confi
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `sort-childObjects` | вид, напр. `Catalog` (batch `;;`), либо пусто | Упорядочить ChildObjects по имени внутри вида. Без значения — все виды, кроме `Subsystem` |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
+12
View File
@@ -39,6 +39,18 @@
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
## sort-childObjects
Упорядочивает объекты в `<ChildObjects>` по имени **внутри вида**. Значение — имя вида (`Catalog`, `Role`, …), batch через `;;`. Без значения обрабатываются все виды, какие есть в файле.
```
-Operation sort-childObjects — все виды, кроме Subsystem
-Operation sort-childObjects -Value "Catalog" — только справочники
-Operation sort-childObjects -Value "Catalog ;; Role"
```
`Subsystem` пропускается, пока вид не назван явно (`-Value "Subsystem"`): сортировка подсистем меняет порядок разделов в панели.
## add-defaultRole / remove-defaultRole / set-defaultRoles
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
+240 -28
View File
@@ -1,10 +1,10 @@
# cf-edit v1.23 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.24 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
[string]$DefinitionFile,
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page","sort-childObjects")]
[string]$Operation,
[string]$Value,
[switch]$NoValidate
@@ -376,6 +376,21 @@ function Import-Fragment([string]$xmlString) {
}
# --- Parse batch value (split by ;;) ---
# Имя вида из пользовательского ввода → каноническое имя или $null.
# Ввод прощающий: регистр не важен, принимается имя каталога выгрузки (Catalogs → Catalog)
# и русское имя вида в единственном и множественном числе.
function Resolve-TypeName([string]$token) {
$key = "$token".Trim()
if (-not $key) { return $null }
foreach ($canon in $script:typeOrder) { if ($canon -eq $key) { return $canon } }
$byDir = $script:dirToType[$key.ToLowerInvariant()]
if ($byDir) { return $byDir }
$ru = $script:ruTypeMap[$key.ToLowerInvariant()]
if ($ru) { return $ru }
return $null
}
function Parse-BatchValue([string]$val) {
$items = @()
foreach ($part in $val.Split(";;")) {
@@ -441,6 +456,169 @@ function Do-ModifyProperty([string]$batchVal) {
}
# --- Operation: add-childObject ---
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
# настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
# оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir))
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
# Сортировка имён компаратором Compare-MetadataNames. В py-порту ту же роль играет
# functools.cmp_to_key — штатный способ отсортировать компаратором; в PS 5.1 его нет,
# поэтому слияние вручную. Порядок обоих портов задаёт один и тот же компаратор.
function Sort-MetadataNames([string[]]$names) {
# Возврат без запятой-обёртки: приёмная сторона всегда пишет @(...), и одноэлементный
# результат остаётся массивом. С `return ,@(...)` @() собрал бы ОДИН объект-массив.
if ($names.Count -le 1) { return $names }
$mid = [int]($names.Count / 2)
$left = @(Sort-MetadataNames $names[0..($mid - 1)])
$right = @(Sort-MetadataNames $names[$mid..($names.Count - 1)])
$out = New-Object System.Collections.ArrayList
$i = 0; $j = 0
while ($i -lt $left.Count -and $j -lt $right.Count) {
if ((Compare-MetadataNames $left[$i] $right[$j]) -le 0) { [void]$out.Add($left[$i]); $i++ }
else { [void]$out.Add($right[$j]); $j++ }
}
while ($i -lt $left.Count) { [void]$out.Add($left[$i]); $i++ }
while ($j -lt $right.Count) { [void]$out.Add($right[$j]); $j++ }
return $out.ToArray()
}
# Упорядочить <ChildObjects> по имени внутри вида.
# Без значения — все виды, кроме Subsystem (порядок подсистем в дереве задаёт порядок
# разделов в панели, пока их не перечислили в <SubsystemsOrder>); явно названный вид
# сортируется в любом случае. Взаимный порядок видов не трогаем: платформа приводит его
# к своему при первой же выгрузке. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы
# и структура файла остаются как были, меняются только имена в строках.
function Do-SortChildObjects([string]$batchVal) {
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
# Ввод прощающий: регистр не важен, принимается и имя каталога (Catalogs → Catalog) —
# в дереве выгрузки виды видны именно во множественном числе.
# Без @(...) на приёме: Parse-BatchValue возвращает ,$items — обёртка, которую @()
# собрал бы как ОДИН объект-массив, и вид не нашёлся бы в $script:typeOrder.
$tokens = @()
if ("$batchVal".Trim()) { $tokens = Parse-BatchValue $batchVal }
$requested = @()
foreach ($token in $tokens) {
$canon = Resolve-TypeName $token
if (-not $canon) { Write-Error "Unknown type '$token'. Valid: $($script:typeOrder -join ', ')"; exit 1 }
$requested += $canon
}
$groups = New-Object System.Collections.Specialized.OrderedDictionary
foreach ($child in $script:childObjsEl.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$ln = $child.get_LocalName()
if (-not $groups.Contains($ln)) { $groups[$ln] = New-Object System.Collections.ArrayList }
[void]$groups[$ln].Add($child)
}
$targets = if ($requested.Count -gt 0) { $requested } else { @($groups.Keys | Where-Object { $_ -cne 'Subsystem' }) }
foreach ($typeName in $targets) {
if (-not $groups.Contains($typeName)) { continue }
$els = $groups[$typeName]
if ($els.Count -lt 2) { continue }
$names = @(foreach ($e in $els) { $e.InnerText })
$ordered = @(Sort-MetadataNames $names)
$same = $true
for ($i = 0; $i -lt $names.Count; $i++) { if ($names[$i] -cne $ordered[$i]) { $same = $false; break } }
if ($same) { continue }
for ($i = 0; $i -lt $els.Count; $i++) { $els[$i].InnerText = $ordered[$i] }
$script:modifyCount++
Info "Sorted: $typeName ($($els.Count))"
}
}
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
# финальный перенос. $null → файл новый (сохранить текущее поведение).
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Detect-XmlStyle([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return $null }
$raw = [System.IO.File]::ReadAllBytes($path)
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
$m = [regex]::Match($head, 'encoding="([^"]+)"')
return @{
bom = $bom
crlf = $body.Contains("`r`n")
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
finalNl = $body.EndsWith("`n")
}
}
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Finalize-XmlText([string]$text, $style) {
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
if ($style -and $style.finalNl) { $text += "`n" }
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
return $text
}
function Do-AddChildObject([string]$batchVal) {
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
@@ -460,6 +638,8 @@ function Do-AddChildObject([string]$batchVal) {
exit 1
}
$typeName = $item.Substring(0, $dotIdx)
$canonType = Resolve-TypeName $typeName
if ($canonType) { $typeName = $canonType }
$objNameVal = $item.Substring($dotIdx + 1)
# Check type is valid
@@ -504,11 +684,11 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
continue
}
# Find insertion point: after last element of same type, or after last element of preceding type
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition.
$byName = ($typeName -cne "Subsystem" -and (Get-NewObjectPosition $script:configDir) -eq "byName")
$insertBefore = $null
$lastSameType = $null
$lastPrecedingType = $null
$currentTypeIdx = -1
$firstLaterType = $null
foreach ($child in $script:childObjsEl.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
@@ -516,17 +696,29 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
if ($childTypeIdx -lt 0) { continue }
if ($child.LocalName -eq $typeName) {
# Same type — check alphabetical order
if ($child.InnerText -gt $objNameVal -and -not $insertBefore) {
# Insert before this element (alphabetical)
# Внутри вида — по newObjectPosition: end (по умолчанию) кладёт после последнего
# объекта того же вида, byName — по имени. Subsystem по имени не упорядочиваем
# никогда: порядок подсистем в дереве задаёт порядок разделов в панели.
$lastSameType = $child
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objNameVal) -gt 0) {
$insertBefore = $child
}
$lastSameType = $child
} elseif ($childTypeIdx -lt $typeIdx) {
$lastPrecedingType = $child
} elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) {
# First element of a later type — insert before it
$insertBefore = $child
} elseif ($childTypeIdx -gt $typeIdx -and -not $firstLaterType) {
$firstLaterType = $child
}
}
if (-not $insertBefore) {
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
# то есть перед его следующим соседом. Через $firstLaterType этого не сделать:
# если видов старше в файле нет, запись уехала бы в самый конец блока,
# за пределы своей группы.
if ($lastSameType) {
$next = $lastSameType.NextSibling
while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling }
$insertBefore = $next
} else {
$insertBefore = $firstLaterType
}
}
@@ -558,6 +750,8 @@ function Do-RemoveChildObject([string]$batchVal) {
exit 1
}
$typeName = $item.Substring(0, $dotIdx)
$canonType = Resolve-TypeName $typeName
if ($canonType) { $typeName = $canonType }
$objNameVal = $item.Substring($dotIdx + 1)
$found = $false
@@ -787,6 +981,29 @@ $script:ruTypeMap = @{
"бот" = "Bot"
"планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage"
# Множественное число: в дереве конфигурации виды подписаны именно так.
"справочники" = "Catalog"
"документы" = "Document"
"перечисления" = "Enum"
"отчёты" = "Report"
"отчеты" = "Report"
"обработки" = "DataProcessor"
"общиеформы" = "CommonForm"
"журналыдокументов" = "DocumentJournal"
"планывидовхарактеристик" = "ChartOfCharacteristicTypes"
"планысчетов" = "ChartOfAccounts"
"планывидоврасчета" = "ChartOfCalculationTypes"
"планывидоврасчёта" = "ChartOfCalculationTypes"
"регистрысведений" = "InformationRegister"
"регистрынакопления" = "AccumulationRegister"
"регистрыбухгалтерии" = "AccountingRegister"
"регистрырасчета" = "CalculationRegister"
"регистрырасчёта" = "CalculationRegister"
"бизнеспроцессы" = "BusinessProcess"
"задачи" = "Task"
"боты" = "Bot"
"планыобмена" = "ExchangePlan"
"хранилищанастроек" = "SettingsStorage"
}
# plural folder → singular type
$script:dirToType = @{}
@@ -1028,11 +1245,16 @@ foreach ($op in $operations) {
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
"set-panels" { Do-SetPanels $opValue }
"set-home-page" { Do-SetHomePage $opValue }
"sort-childObjects" { Do-SortChildObjects $opValueStr }
default { Write-Error "Unknown operation: $opName"; exit 1 }
}
}
# --- Save ---
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$xmlStyle = Detect-XmlStyle $resolvedPath
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
@@ -1043,22 +1265,12 @@ $writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$script:xmlDoc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
$text = Finalize-XmlText $text $xmlStyle
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
$writeBom = ($null -eq $xmlStyle) -or $xmlStyle.bom
[System.IO.File]::WriteAllText($resolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
Info "Saved: $resolvedPath"
# --- Auto-validate ---
+190 -26
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# cf-edit v1.23 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.24 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import functools
import json
import os
import re
@@ -353,6 +354,122 @@ SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCat
REF_PROPS = ["DefaultLanguage"]
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.path.abspath(cfg_dir or ".")) or _sg_find_v8project(os.getcwd())
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
RU_TYPE_MAP = {
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
"плансчетов": "ChartOfAccounts",
"планвидоврасчета": "ChartOfCalculationTypes",
"планвидоврасчёта": "ChartOfCalculationTypes",
"регистрсведений": "InformationRegister",
"регистрнакопления": "AccumulationRegister",
"регистрбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
# Множественное число: в дереве конфигурации виды подписаны именно так.
"справочники": "Catalog", "документы": "Document", "перечисления": "Enum",
"отчёты": "Report", "отчеты": "Report", "обработки": "DataProcessor",
"общиеформы": "CommonForm", "журналыдокументов": "DocumentJournal",
"планывидовхарактеристик": "ChartOfCharacteristicTypes",
"планысчетов": "ChartOfAccounts",
"планывидоврасчета": "ChartOfCalculationTypes",
"планывидоврасчёта": "ChartOfCalculationTypes",
"регистрысведений": "InformationRegister",
"регистрынакопления": "AccumulationRegister",
"регистрыбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister", "регистрырасчета": "CalculationRegister",
"регистрырасчёта": "CalculationRegister",
"бизнеспроцессы": "BusinessProcess",
"боты": "Bot",
"задачи": "Task", "планыобмена": "ExchangePlan",
"хранилищанастроек": "SettingsStorage",
}
def resolve_type_name(token):
"""Имя вида из пользовательского ввода → каноническое имя или None.
Ввод прощающий: регистр не важен, принимается имя каталога выгрузки
(Catalogs → Catalog) и русское имя вида в единственном и множественном числе.
"""
key = (token or "").strip().lower()
if not key:
return None
for canon in TYPE_ORDER:
if canon.lower() == key:
return canon
for canon, dir_name in TYPE_TO_DIR.items():
if dir_name.lower() == key:
return canon
return RU_TYPE_MAP.get(key)
def localname(el):
return etree.QName(el.tag).localname
@@ -498,7 +615,7 @@ def main():
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
parser.add_argument("-ConfigPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page", "sort-childObjects"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = ci_parse_args(parser)
@@ -637,7 +754,7 @@ def main():
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
obj_name_val = item[dot_idx + 1:]
if type_name not in TYPE_ORDER:
@@ -674,8 +791,15 @@ def main():
warn(f"Already exists: {type_name}.{obj_name_val}")
continue
# Find insertion point
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт
# порядок разделов в панели, пока их не перечислили в <SubsystemsOrder>.
by_name = (type_name != "Subsystem"
and get_new_object_position(config_dir) == "byName")
insert_before = None
last_same = None
first_later = None
for child in child_objs_el:
if not isinstance(child.tag, str):
continue
@@ -685,10 +809,24 @@ def main():
child_type_idx = TYPE_ORDER.index(child_type_name)
if child_type_name == type_name:
if (child.text or "") > obj_name_val and insert_before is None:
last_same = child
if (by_name and insert_before is None
and compare_metadata_names(child.text or "", obj_name_val) > 0):
insert_before = child
elif child_type_idx > type_idx and insert_before is None:
insert_before = child
elif child_type_idx > type_idx and first_later is None:
first_later = child
if insert_before is None:
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
# то есть перед его следующим соседом. Через first_later этого не сделать:
# если видов старше в файле нет, запись уехала бы в самый конец блока,
# за пределы своей группы.
if last_same is not None:
siblings = [c for c in child_objs_el if isinstance(c.tag, str)]
pos = siblings.index(last_same)
insert_before = siblings[pos + 1] if pos + 1 < len(siblings) else None
else:
insert_before = first_later
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
new_el.text = obj_name_val
@@ -701,6 +839,48 @@ def main():
add_count += 1
info(f"Added: {type_name}.{obj_name_val}")
def do_sort_child_objects(batch_val):
"""Упорядочить <ChildObjects> по имени внутри вида.
Без значения — все виды, кроме Subsystem (порядок подсистем в дереве задаёт порядок
разделов в панели, пока их не перечислили в <SubsystemsOrder>); явно названный вид
сортируется в любом случае. Взаимный порядок видов не трогаем: платформа приводит его
к своему при первой же выгрузке. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы —
отступы и структура файла остаются как были, меняются только имена в строках.
"""
nonlocal modify_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
requested = []
for token in (parse_batch_value(batch_val) if str(batch_val or "").strip() else []):
canon = resolve_type_name(token)
if canon is None:
print(f"Unknown type '{token}'. Valid: {', '.join(TYPE_ORDER)}", file=sys.stderr)
sys.exit(1)
requested.append(canon)
groups = {}
for child in child_objs_el:
if not isinstance(child.tag, str):
continue
groups.setdefault(localname(child), []).append(child)
targets = requested or [t for t in groups if t != "Subsystem"]
for type_name in targets:
els = groups.get(type_name, [])
if len(els) < 2:
continue
names = [e.text or "" for e in els]
ordered = sorted(names, key=functools.cmp_to_key(compare_metadata_names))
if names == ordered:
continue
for el, name in zip(els, ordered):
el.text = name
modify_count += 1
info(f"Sorted: {type_name} ({len(els)})")
def do_remove_child_object(batch_val):
nonlocal remove_count
if child_objs_el is None:
@@ -713,7 +893,7 @@ def main():
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
obj_name_val = item[dot_idx + 1:]
found = False
@@ -933,24 +1113,6 @@ def main():
info(f"Wrote panel layout: {cai_path}")
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
RU_TYPE_MAP = {
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
"плансчетов": "ChartOfAccounts",
"планвидоврасчета": "ChartOfCalculationTypes",
"планвидоврасчёта": "ChartOfCalculationTypes",
"регистрсведений": "InformationRegister",
"регистрнакопления": "AccumulationRegister",
"регистрбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
}
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
@@ -1130,6 +1292,8 @@ def main():
do_set_panels(op_value)
elif op_key == "set-home-page":
do_set_home_page(op_value)
elif op_key == "sort-childobjects":
do_sort_child_objects(op_value if isinstance(op_value, str) else str(op_value))
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
+138 -56
View File
@@ -1,4 +1,4 @@
# cfe-borrow v1.33 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.34 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -1347,32 +1347,22 @@ function Register-FormInObject {
}
# Save object XML
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$style2 = Detect-XmlStyle $objFile
$settings2 = New-Object System.Xml.XmlWriterSettings
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings2.Indent = $false
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream2 = New-Object System.IO.MemoryStream
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
$objDoc.Save($writer2)
$writer2.Flush(); $writer2.Close()
$bytes2 = $memStream2.ToArray()
$text2 = [System.Text.Encoding]::UTF8.GetString($memStream2.ToArray())
$memStream2.Close()
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
$text2 = Finalize-XmlText $text2 $style2
$writeBom2 = ($null -eq $style2) -or $style2.bom
[System.IO.File]::WriteAllText($objFile, $text2, (New-Object System.Text.UTF8Encoding($writeBom2)))
Info " Registered form in: $objFile"
}
@@ -1885,6 +1875,9 @@ function Merge-AttributesIntoObject {
}
# Save via text manipulation to avoid namespace issues with InnerXml
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$style3 = Detect-XmlStyle $objFile
$settings3 = New-Object System.Xml.XmlWriterSettings
$settings3.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings3.Indent = $false
@@ -1893,28 +1886,15 @@ function Merge-AttributesIntoObject {
$writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3)
$objDoc.Save($writer3)
$writer3.Flush(); $writer3.Close()
$bytes3 = $memStream3.ToArray()
$text3 = [System.Text.Encoding]::UTF8.GetString($memStream3.ToArray())
$memStream3.Close()
$text3 = [System.Text.Encoding]::UTF8.GetString($bytes3)
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
# Стоит ДО Finalize-XmlText, чтобы схлопывание пустых тегов накрыло и вставленные реквизиты.
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
$text3 = Finalize-XmlText $text3 $style3
$writeBom3 = ($null -eq $style3) -or $style3.bom
[System.IO.File]::WriteAllText($objFile, $text3, (New-Object System.Text.UTF8Encoding($writeBom3)))
Info " Merged $added attribute(s) into: $objFile"
}
}
@@ -2230,6 +2210,114 @@ function Build-BorrowedObjectXml {
}
# --- 13. Helper: add object to extension ChildObjects ---
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
# финальный перенос. $null → файл новый (сохранить текущее поведение).
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Detect-XmlStyle([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return $null }
$raw = [System.IO.File]::ReadAllBytes($path)
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
$m = [regex]::Match($head, 'encoding="([^"]+)"')
return @{
bom = $bom
crlf = $body.Contains("`r`n")
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
finalNl = $body.EndsWith("`n")
}
}
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Finalize-XmlText([string]$text, $style) {
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
if ($style -and $style.finalNl) { $text += "`n" }
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
return $text
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
# настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
# оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir))
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
function Add-ToChildObjects {
param([string]$typeName, [string]$objName)
@@ -2255,7 +2343,12 @@ function Add-ToChildObjects {
}
}
# Find insertion point: after last element of same type, or before first element of later type
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition: end
# (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени. Так же
# заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects не отсортирован.
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт порядок
# разделов в панели.
$byName = ($typeName -cne "Subsystem" -and (Get-NewObjectPosition $extDir) -eq "byName")
$insertBefore = $null
$lastSameType = $null
@@ -2265,8 +2358,7 @@ function Add-ToChildObjects {
if ($childTypeIdx -lt 0) { continue }
if ($child.LocalName -eq $typeName) {
# Same type -- check alphabetical order
if ($child.InnerText -gt $objName -and -not $insertBefore) {
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objName) -gt 0) {
$insertBefore = $child
}
$lastSameType = $child
@@ -2439,32 +2531,22 @@ while ($true) {
}
# --- 15. Save modified Configuration.xml ---
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$style = Detect-XmlStyle $extResolvedPath
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$script:xmlDoc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
$text = Finalize-XmlText $text $style
$writeBom = ($null -eq $style) -or $style.bom
[System.IO.File]::WriteAllText($extResolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
Info "Saved: $extResolvedPath"
# --- 16. Summary ---
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# cfe-borrow v1.33 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.34 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
@@ -202,6 +203,82 @@ def decode_numeric_entities(s):
return s
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.path.abspath(cfg_dir or ".")) or _sg_find_v8project(os.getcwd())
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
def localname(el):
return etree.QName(el.tag).localname
@@ -1035,6 +1112,13 @@ def main():
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
return
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
# Так же, как заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects
# не отсортирован. Subsystem по имени не упорядочиваем никогда: порядок подсистем
# в дереве задаёт порядок разделов в панели.
by_name = (type_name != "Subsystem"
and get_new_object_position(ext_dir) == "byName")
insert_before = None
for child in child_objs_el:
if not isinstance(child.tag, str):
@@ -1045,7 +1129,8 @@ def main():
child_type_idx = TYPE_ORDER.index(child_type_name)
if child_type_name == type_name:
if (child.text or "") > obj_name and insert_before is None:
if (by_name and insert_before is None
and compare_metadata_names(child.text or "", obj_name) > 0):
insert_before = child
elif child_type_idx > type_idx and insert_before is None:
insert_before = child
+1
View File
@@ -76,6 +76,7 @@ allowed-tools:
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
| `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` |
| `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
@@ -1,4 +1,4 @@
# meta-compile v1.99 — Compile 1C metadata object from JSON
# meta-compile v1.100 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -5234,8 +5234,69 @@ if ($commands -and $commands.Count -gt 0) {
# --- 17. Register in Configuration.xml ---
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
# настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
# оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir))
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
# meta-compile, копия — role-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# Возвращает исход: added | already | no-childobj | no-config.
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
@@ -5255,56 +5316,56 @@ function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [st
if ($e.InnerText -eq $ChildName) { return "already" }
}
$newElem = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
$newElem.InnerText = $ChildName
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
$enc = New-Object System.Text.UTF8Encoding($true)
if ($existing.Count -gt 0) {
# Insert after last existing element of same type
$lastElem = $existing[$existing.Count - 1]
$newWs = $doc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
} else {
# No existing elements of this type — insert before closing whitespace.
# Самозакрытый <ChildObjects/> попадает сюда же: LastChild пуст, идёт ветка AppendChild.
$lastChild = $childObjects.LastChild
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$newWs = $doc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($newElem) | Out-Null
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t")) | Out-Null
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
if (-not $block.Success) {
# Самозакрытый <ChildObjects/> раскрываем первой записью
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
if (-not $empty.Success) { return "no-childobj" }
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if ($ChildTag -cne "Subsystem" -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
while ($m.Success) {
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
$m = $m.NextMatch()
}
}
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
# `encoding="UTF-8"` и `<a/>`.
$cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
$doc.Save($writer)
$writer.Flush(); $writer.Close()
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n") { "`n" } else { "`r`n" }
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($ParentXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
$closeSame = "</$ChildTag>"
$blockEnd = $block.Index + $block.Length
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
if ($lastSame -ge 0) {
# После последнего объекта того же вида (группы по видам сохраняются)
$insertAt = $lastSame + $closeSame.Length
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
} else {
# Объектов этого вида ещё нет: новая строка перед </ChildObjects>,
# отступ закрывающего тега переиспользуется
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
}
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.99 — Compile 1C metadata object from JSON
# meta-compile v1.100 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -5209,10 +5209,71 @@ if commands:
# 17. Register in Configuration.xml
# ---------------------------------------------------------------------------
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end после последнего объекта того же вида
(так дописывает Конфигуратор); byName по имени среди объектов того же вида.
Файл ищем от каталога конфигурации и лишь потом от cwd в отличие от support-guard:
настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.path.abspath(cfg_dir or ".")) or _sg_find_v8project(os.getcwd())
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
"""Регистрация объекта в <ChildObjects> родительского XML.
Общая реализация: эталон meta-compile, копия role-compile.
Общая реализация: эталон meta-compile, копии role-compile, xdto-compile.
Реестр семьи: tests/skills/check-inline-drift.mjs.
Возвращает исход: added | already | no-childobj | no-config.
"""
@@ -5263,6 +5324,20 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
write_utf8_bom(parent_xml_path, new_content)
return 'added'
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if (child_tag != 'Subsystem'
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
for m in line_rx.finditer(config_content, block.start(), block.end()):
if compare_metadata_names(m.group(2), child_name) > 0:
new_content = (config_content[:m.start()]
+ f'{m.group(1)}{entry}{eol}'
+ config_content[m.start():])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
close_same = f'</{child_tag}>'
last_same = config_content.rfind(close_same, block.start(), block.end())
if last_same != -1:
@@ -1,4 +1,4 @@
# role-compile v1.32 — Compile 1C role from JSON
# role-compile v1.33 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -1185,8 +1185,69 @@ $enc = New-Object System.Text.UTF8Encoding($true)
# --- 12. Register in Configuration.xml ---
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
# настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
# оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir))
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
# meta-compile, копия — role-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# Возвращает исход: added | already | no-childobj | no-config.
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
@@ -1206,56 +1267,56 @@ function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [st
if ($e.InnerText -eq $ChildName) { return "already" }
}
$newElem = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
$newElem.InnerText = $ChildName
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
$enc = New-Object System.Text.UTF8Encoding($true)
if ($existing.Count -gt 0) {
# Insert after last existing element of same type
$lastElem = $existing[$existing.Count - 1]
$newWs = $doc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
} else {
# No existing elements of this type — insert before closing whitespace.
# Самозакрытый <ChildObjects/> попадает сюда же: LastChild пуст, идёт ветка AppendChild.
$lastChild = $childObjects.LastChild
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$newWs = $doc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($newElem) | Out-Null
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t")) | Out-Null
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
if (-not $block.Success) {
# Самозакрытый <ChildObjects/> раскрываем первой записью
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
if (-not $empty.Success) { return "no-childobj" }
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if ($ChildTag -cne "Subsystem" -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
while ($m.Success) {
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
$m = $m.NextMatch()
}
}
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
# `encoding="UTF-8"` и `<a/>`.
$cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
$doc.Save($writer)
$writer.Flush(); $writer.Close()
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n") { "`n" } else { "`r`n" }
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($ParentXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
$closeSame = "</$ChildTag>"
$blockEnd = $block.Index + $block.Length
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
if ($lastSame -ge 0) {
# После последнего объекта того же вида (группы по видам сохраняются)
$insertAt = $lastSame + $closeSame.Length
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
} else {
# Объектов этого вида ещё нет: новая строка перед </ChildObjects>,
# отступ закрывающего тега переиспользуется
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
}
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# role-compile v1.32 — Compile 1C role from JSON
# role-compile v1.33 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -1056,10 +1056,71 @@ def parse_object_entry(entry):
return {'Name': obj_name, 'Rights': rights}
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.path.abspath(cfg_dir or ".")) or _sg_find_v8project(os.getcwd())
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
"""Регистрация объекта в <ChildObjects> родительского XML.
Общая реализация: эталон — meta-compile, копия — role-compile.
Общая реализация: эталон — meta-compile, копии — role-compile, xdto-compile.
Реестр семьи: tests/skills/check-inline-drift.mjs.
Возвращает исход: added | already | no-childobj | no-config.
"""
@@ -1110,6 +1171,20 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
write_utf8_bom(parent_xml_path, new_content)
return 'added'
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if (child_tag != 'Subsystem'
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
for m in line_rx.finditer(config_content, block.start(), block.end()):
if compare_metadata_names(m.group(2), child_name) > 0:
new_content = (config_content[:m.start()]
+ f'{m.group(1)}{entry}{eol}'
+ config_content[m.start():])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
close_same = f'</{child_tag}>'
last_same = config_content.rfind(close_same, block.start(), block.end())
if last_same != -1:
@@ -1,4 +1,4 @@
# subsystem-compile v1.30 — Create 1C subsystem from JSON definition
# subsystem-compile v1.31 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -690,77 +690,32 @@ function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [st
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $ns)
if (-not $childObjects) { return "no-childobj" }
# Check for self-closing tag
$isSelfClosing = (-not $childObjects.HasChildNodes) -or ($childObjects.IsEmpty)
# Check if already registered
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $ChildTag -and $child.InnerText -eq $ChildName) {
return "already"
}
}
$newEl = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
$newEl.InnerText = $ChildName
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт.
# Правим чужой файл, значит наследуем его стиль (#44/#46/#47). DOM выше — только на чтение.
$rawText = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
$eol = if ($rawText.Contains("`r`n")) { "`r`n" } else { "`n" }
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
if ($isSelfClosing) {
# Expand self-closing tag
$parentIndent = ""
$prev = $childObjects.PreviousSibling
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
if ($prev.Value -match '(\t+)$') { $parentIndent = $Matches[1] }
}
$childIndent = "$parentIndent`t"
$ws1 = $doc.CreateWhitespace("`r`n$childIndent")
$ws2 = $doc.CreateWhitespace("`r`n$parentIndent")
$childObjects.AppendChild($ws1) | Out-Null
$childObjects.AppendChild($newEl) | Out-Null
$childObjects.AppendChild($ws2) | Out-Null
$empty = [regex]::Match($rawText, '<ChildObjects\s*/>')
if ($empty.Success) {
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
$rawText = $rawText.Substring(0, $empty.Index) + $replacement + $rawText.Substring($empty.Index + $empty.Length)
} else {
# Insert before trailing whitespace
$childIndent = "`t`t`t"
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
if ($child.Value -match '^\r?\n(\t+)') { $childIndent = $Matches[1]; break }
}
}
$trailing = $childObjects.LastChild
$ws = $doc.CreateWhitespace("`r`n$childIndent")
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
$childObjects.InsertBefore($ws, $trailing) | Out-Null
$childObjects.InsertBefore($newEl, $trailing) | Out-Null
} else {
$childObjects.AppendChild($ws) | Out-Null
$childObjects.AppendChild($newEl) | Out-Null
}
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка по голому
# '</ChildObjects>' удваивала бы уже присутствующий отступ строки.
$cm = [regex]::Match($rawText, '([ ]*)</ChildObjects>')
if (-not $cm.Success) { return "no-childobj" }
$rawText = $rawText.Substring(0, $cm.Index) + $cm.Groups[1].Value + "`t" + $entry + $eol + $cm.Groups[1].Value + "</ChildObjects>" + $rawText.Substring($cm.Index + $cm.Length)
}
# Save parent XML
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$doc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $ParentXmlPath) -and ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($ParentXmlPath, $text, (New-Object System.Text.UTF8Encoding($true)))
[System.IO.File]::WriteAllText($ParentXmlPath, $rawText, (New-Object System.Text.UTF8Encoding($true)))
return "added"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# subsystem-compile v1.30 — Create 1C subsystem from JSON definition
# subsystem-compile v1.31 — Create 1C subsystem from JSON definition
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -489,16 +489,19 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
# Правку ведём по сырому тексту, а не сериализацией ET: она не сохраняет отступы
# и теряет xmlns, объявленные только внутри значений атрибутов (#38).
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
if '<ChildObjects/>' in raw_text:
empty = re.search(r'<ChildObjects\s*/>', raw_text)
if empty is not None:
replacement = '<ChildObjects>' + eol + f'\t\t\t{entry}' + eol + '\t\t</ChildObjects>'
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
elif '</ChildObjects>' in raw_text:
raw_text = raw_text[:empty.start()] + replacement + raw_text[empty.end():]
else:
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
raw_text = re.sub(r'([ \t]*)</ChildObjects>',
lambda m: m.group(1) + '\t' + entry + eol + m.group(1) + '</ChildObjects>',
raw_text, count=1)
cm = re.search(r'([ \t]*)</ChildObjects>', raw_text)
if cm is None:
return 'no-childobj'
raw_text = (raw_text[:cm.start()] + cm.group(1) + '\t' + entry + eol
+ cm.group(1) + '</ChildObjects>' + raw_text[cm.end():])
write_utf8_bom(parent_xml_path, raw_text)
return 'added'
@@ -1,4 +1,4 @@
# xdto-compile v1.12 — Build a 1C XDTO package from an XML Schema (XSD) (+support-guard: общая реализация вместо урезанной)
# xdto-compile v1.13 — Build a 1C XDTO package from an XML Schema (XSD) (+support-guard: общая реализация вместо урезанной)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -988,69 +988,149 @@ if ($declaredImports.Count -gt 0 -and (Test-Path $xdtoRootDir)) {
}
}
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
$regResult = "no-config"
if (Test-Path $configXmlPath) {
$configDoc = New-Object System.Xml.XmlDocument
$configDoc.PreserveWhitespace = $true
$configDoc.Load($configXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
$nsMgr.AddNamespace("md", $MD_NS)
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
if ($childObjects) {
$existing = $childObjects.SelectNodes("md:XDTOPackage", $nsMgr)
$already = $false
foreach ($e in $existing) { if ($e.InnerText -eq $Name) { $already = $true; break } }
if ($already) {
$regResult = "already"
} else {
$newElem = $configDoc.CreateElement("XDTOPackage", $MD_NS)
$newElem.InnerText = $Name
if ($existing.Count -gt 0) {
$lastElem = $existing[$existing.Count - 1]
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
} else {
$lastChild = $childObjects.LastChild
if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($newElem) | Out-Null
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
function Esc-XmlText {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от каталога конфигурации и лишь потом от cwd — в отличие от support-guard:
# настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
# оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir))
if (-not $pj) { $pj = Find-V8Project (Get-Location).Path }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
$cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
$configDoc.Save($writer)
$writer.Flush(); $writer.Close()
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
$regResult = "added"
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# Возвращает исход: added | already | no-childobj | no-config.
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($ParentXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $nsMgr)
if (-not $childObjects) { return "no-childobj" }
$existing = $childObjects.SelectNodes("md:$ChildTag", $nsMgr)
foreach ($e in $existing) {
if ($e.InnerText -eq $ChildName) { return "already" }
}
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
$enc = New-Object System.Text.UTF8Encoding($true)
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
if (-not $block.Success) {
# Самозакрытый <ChildObjects/> раскрываем первой записью
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
if (-not $empty.Success) { return "no-childobj" }
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if ($ChildTag -cne "Subsystem" -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
while ($m.Success) {
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
$m = $m.NextMatch()
}
}
$closeSame = "</$ChildTag>"
$blockEnd = $block.Index + $block.Length
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
if ($lastSame -ge 0) {
# После последнего объекта того же вида (группы по видам сохраняются)
$insertAt = $lastSame + $closeSame.Length
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
} else {
# Объектов этого вида ещё нет: новая строка перед </ChildObjects>,
# отступ закрывающего тега переиспользуется
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
}
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
$regResult = Register-InChildObjects $configXmlPath "Configuration" "XDTOPackage" $Name
# --- Report ---
$typeCount = 0
@@ -1069,5 +1149,6 @@ if ($script:warnings.Count -gt 0) {
switch ($regResult) {
"added" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> добавлен в ChildObjects" }
"already" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> уже зарегистрирован" }
"no-childobj" { [Console]::Error.WriteLine("ПРЕДУПРЕЖДЕНИЕ: Configuration.xml найден, но <ChildObjects> не найден") }
"no-config" { Write-Host " Configuration.xml не найден — регистрация пропущена" }
}
@@ -1,4 +1,4 @@
# xdto-compile v1.12 — Build a 1C XDTO package from an XML Schema (XSD) (Python port) (+support-guard: общая реализация вместо урезанной)
# xdto-compile v1.13 — Build a 1C XDTO package from an XML Schema (XSD) (Python port) (+support-guard: общая реализация вместо урезанной)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -6,6 +6,7 @@ import os
import re
import sys
import uuid
import xml.etree.ElementTree as ET
from lxml import etree
@@ -1093,43 +1094,168 @@ if declared_imports and os.path.isdir(xdto_root_dir):
warn(f'Импорт "{imp}" не разрешается: пакета с таким namespace в конфигурации нет. '
"Платформа отвергнет пакет при обновлении — соберите зависимость первой")
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
reg_result = "no-config"
if os.path.exists(config_xml):
with open(config_xml, "rb") as f:
raw = f.read()
had_bom = raw.startswith(b"\xef\xbb\xbf")
cfg_doc = _parse_xml(config_xml)
child_objects = cfg_doc.find(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
if child_objects is not None:
existing = child_objects.findall(f"{{{MD_NS}}}XDTOPackage")
if any((e.text or "") == name for e in existing):
reg_result = "already"
else:
new_elem = etree.SubElement(child_objects, f"{{{MD_NS}}}XDTOPackage")
new_elem.text = name
if existing:
last = existing[-1]
new_elem.tail = last.tail
child_objects.remove(new_elem)
last.addnext(new_elem)
def esc_xml_text(s):
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw).
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end после последнего объекта того же вида
(так дописывает Конфигуратор); byName по имени среди объектов того же вида.
Файл ищем от каталога конфигурации и лишь потом от cwd в отличие от support-guard:
настройка принадлежит выгрузке, а рабочим каталогом при вызове навыка почти всегда
оказывается чужой проект со своим .v8-project.json, и он перекрыл бы нужный.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.path.abspath(cfg_dir or ".")) or _sg_find_v8project(os.getcwd())
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
new_elem.tail = child_objects.text
data = etree.tostring(cfg_doc, xml_declaration=True, encoding="UTF-8")
# lxml пишет декларацию в ОДИНАРНЫХ кавычках, платформа и PS-порт — в двойных.
data = data.replace(b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="UTF-8"?>')
# Парсер XML по спецификации схлопывает CRLF в LF, поэтому tostring отдаёт
# LF-документ. Возвращаем EOL исходного файла: правка существующего файла
# сохраняет его стиль (#44/#46/#47). Правило то же, что у _detect_xml_style
# и у $targetEol в PS-порту: есть CRLF → CRLF.
src_eol = b"\r\n" if b"\r\n" in raw else b"\n"
data = data.replace(b"\r\n", b"\n").replace(b"\n", src_eol)
if had_bom:
data = b"\xef\xbb\xbf" + data
with open(config_xml, "wb") as f:
f.write(data)
reg_result = "added"
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
"""Регистрация объекта в <ChildObjects> родительского XML.
Общая реализация: эталон meta-compile, копии role-compile, xdto-compile.
Реестр семьи: tests/skills/check-inline-drift.mjs.
Возвращает исход: added | already | no-childobj | no-config.
"""
if not os.path.isfile(parent_xml_path):
return 'no-config'
# Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation)
with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
config_content = f.read()
ns = 'http://v8.1c.ru/8.3/MDClasses'
# ET is used ONLY read-only here: to locate ChildObjects and detect a duplicate.
# We deliberately do NOT re-serialize Configuration.xml with ElementTree.write():
# it drops every xmlns declaration used only inside attribute VALUES (e.g.
# xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees such
# prefixes in element/attribute names. The dropped declaration makes XDTO read the
# value as anyType and Designer refuses to load the file (issue #38). Registration is
# therefore done by raw-text insertion, preserving BOM, EOL and all namespaces
# byte-for-byte (same approach as subsystem-compile).
tree = ET.parse(parent_xml_path)
root = tree.getroot()
child_objects = root.find(f'{{{ns}}}{parent_tag}/{{{ns}}}ChildObjects')
if child_objects is None:
# Try direct path
parent_elem = root.find(f'{{{ns}}}{parent_tag}')
if parent_elem is not None:
child_objects = parent_elem.find(f'{{{ns}}}ChildObjects')
if child_objects is None:
return 'no-childobj'
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
if any((e.text or '').strip() == child_name for e in existing):
return 'already'
eol = '\r\n' if '\r\n' in config_content else '\n'
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
block = re.search(r'<ChildObjects\s*>.*?</ChildObjects>', config_content, re.S)
if block is None:
# Empty self-closing <ChildObjects/> => open it with the first entry.
empty = re.search(r'<ChildObjects\s*/>', config_content)
if empty is None:
return 'no-childobj'
replacement = f'<ChildObjects>{eol}\t\t\t{entry}{eol}\t\t</ChildObjects>'
new_content = config_content[:empty.start()] + replacement + config_content[empty.end():]
write_utf8_bom(parent_xml_path, new_content)
return 'added'
# byName: перед первым объектом того же вида, чьё имя больше нового. Subsystem — никогда:
# порядок подсистем в дереве задаёт порядок разделов в панели, пока их не перечислили в
# <SubsystemsOrder> файла Ext/CommandInterface.xml (платформа этот список сама не заводит).
if (child_tag != 'Subsystem'
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
for m in line_rx.finditer(config_content, block.start(), block.end()):
if compare_metadata_names(m.group(2), child_name) > 0:
new_content = (config_content[:m.start()]
+ f'{m.group(1)}{entry}{eol}'
+ config_content[m.start():])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
close_same = f'</{child_tag}>'
last_same = config_content.rfind(close_same, block.start(), block.end())
if last_same != -1:
# After the last element of the same type (keeps them grouped).
insert_at = last_same + len(close_same)
new_content = (config_content[:insert_at]
+ f'{eol}\t\t\t{entry}'
+ config_content[insert_at:])
else:
# No element of this type yet: new line before </ChildObjects>,
# reusing the block's existing closing indent for </ChildObjects>.
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
new_content = (config_content[:close_at]
+ f'\t{entry}{eol}\t\t'
+ config_content[close_at:])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
reg_result = register_in_childobjects(config_xml, "Configuration", "XDTOPackage", name)
type_count = sum(1 for c in pkg_node.children if c.tag in ("objectType", "valueType"))
print(f"✓ Пакет XDTO собран: {name}")
@@ -1147,5 +1273,7 @@ if reg_result == "added":
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> добавлен в ChildObjects")
elif reg_result == "already":
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> уже зарегистрирован")
elif reg_result == "no-childobj":
print("ПРЕДУПРЕЖДЕНИЕ: Configuration.xml найден, но <ChildObjects> не найден", file=sys.stderr)
else:
print(" Configuration.xml не найден — регистрация пропущена")