mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-07 10:40:52 +03:00
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:
co-authored by
Sergei Pleshanov
Claude Opus 5
parent
7409bacd47
commit
c7b0dce151
@@ -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('&','&').Replace('<','<').Replace('>','>')
|
||||
}
|
||||
|
||||
# Куда навык ставит новую запись в <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('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
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 не найден — регистрация пропущена")
|
||||
|
||||
Reference in New Issue
Block a user