fix(cfe-borrow): вставка только в собственный ChildObjects объекта

Заимствованное содержимое вставлялось текстом по ВСЕМ вхождениям
<ChildObjects>, а у объекта с табличными частями свой контейнер один, но
вхождений столько же, сколько ТЧ. При повторном заимствовании по тому же
объекту ps1 выдавал невалидный XML («mismatched tag»), py — валидный, но с
табличными частями внутри табличной части и дублем ТЧ.

Отдельно ветка «Replace empty ChildObjects» в ps1 переписывала КАЖДЫЙ блок
содержимым первого — теряла данные.

Свой <ChildObjects> закрывается в файле последним: объект в файле один,
вложенные ТЧ закрываются раньше. Правило вынесено в общую функцию каждого
порта, на неё переведены все места вставки.

Дедуп при повторном заимствовании брал имена регуляркой «первый
<ChildObjects> до первого </ChildObjects>» — у объекта с ТЧ этот отрезок
обрывается на закрытии первой ТЧ: захватывает имена её колонок и теряет всё,
что идёт после. Заменён разбором прямых детей своего контейнера. В py дедуп
дополнительно сканировал все <Name> во всём файле, включая имя самого объекта.

Проверено: два заимствования подряд по одному документу дают в обоих портах
валидный XML, 5 табличных частей на верхнем уровне, вложенности и дублей нет;
выходы портов отличаются только свежими uuid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-14 14:13:20 +03:00
co-authored by Claude Opus 5
parent 8c915df0d9
commit b5662a380d
2 changed files with 101 additions and 51 deletions
@@ -1,4 +1,4 @@
# cfe-borrow v1.27 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.28 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -75,6 +75,50 @@ function Rewrite-ChoiceParameterLinks {
return $xml
}
# Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
# заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
# у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
# теряет то, что идёт после неё.
function Get-OwnChildObjectNames {
param([string]$objFile)
$names = @{}
if (-not (Test-Path -LiteralPath $objFile)) { return $names }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
try { $doc.Load($objFile) } catch { return $names }
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return $names }
$childObjs = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
if (-not $childObjs) { return $names }
foreach ($child in $childObjs.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if ($nameNode) { $names[$nameNode.InnerText.Trim()] = $true }
}
return $names
}
# Вставка в СОБСТВЕННЫЙ <ChildObjects> объекта. Свой контейнер закрывается в файле последним:
# объект в файле один, а вложенные <ChildObjects> табличных частей закрываются раньше. Замена по
# всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ.
function Insert-IntoOwnChildObjects {
param([string]$text, [string]$content)
$closeIdx = $text.LastIndexOf('</ChildObjects>')
if ($closeIdx -ge 0) {
return $text.Substring(0, $closeIdx) + "${content}`r`n`t`t" + $text.Substring($closeIdx)
}
# Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже)
$selfMatches = [regex]::Matches($text, '<ChildObjects\s*/>')
if ($selfMatches.Count -eq 0) { return $text }
$m = $selfMatches[$selfMatches.Count - 1]
return $text.Substring(0, $m.Index) + "<ChildObjects>${content}`r`n`t`t</ChildObjects>" + $text.Substring($m.Index + $m.Length)
}
# --- 1. Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
@@ -1597,14 +1641,9 @@ function Merge-AttributesIntoObject {
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Insert attributes — handle both <ChildObjects/> and <ChildObjects>...</ChildObjects>.
# Самозакрытый элемент раскрывается здесь же, а не пробельным узлом в DOM: тот давал
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
if ($text3 -match '<ChildObjects\s*/>') {
$text3 = [regex]::Replace($text3, '<ChildObjects\s*/>', "<ChildObjects>${allAttrXml}`r`n`t`t</ChildObjects>")
} else {
$text3 = $text3.Replace('</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>")
}
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
@@ -1664,12 +1703,7 @@ function Borrow-MainAttribute {
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
$existingChildNames = @{}
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
$existingChildNames[$nm.Groups[1].Value] = $true
}
}
$existingChildNames = Get-OwnChildObjectNames $objFile
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
@@ -1701,17 +1735,9 @@ function Borrow-MainAttribute {
}
}
# Replace empty ChildObjects with adopted content
# Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
if ($adoptedContent) {
# Handle <ChildObjects/> (self-closing)
if ($objContent -match '<ChildObjects\s*/>') {
$objContent = $objContent -replace '<ChildObjects\s*/>', "<ChildObjects>`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elseif ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
$existingInner = $Matches[1]
$objContent = $objContent -replace '(?s)<ChildObjects>(.*?)</ChildObjects>', "<ChildObjects>${existingInner}`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
$objContent = Insert-IntoOwnChildObjects $objContent "`r`n${adoptedContent}"
}
$encBom = New-Object System.Text.UTF8Encoding($true)
+51 -27
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.27 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.28 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -94,6 +94,50 @@ def rewrite_choice_parameter_links(xml, attr_uuids):
return xml
def get_own_child_object_names(obj_file):
"""Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
теряет то, что идёт после неё."""
names = set()
try:
tree = etree.parse(obj_file)
except Exception:
return names
root = tree.getroot()
obj_el = next((c for c in root if isinstance(c.tag, str)), None)
if obj_el is None:
return names
child_objs = next((c for c in obj_el if isinstance(c.tag, str) and localname(c) == "ChildObjects"), None)
if child_objs is None:
return names
for child in child_objs:
if not isinstance(child.tag, str):
continue
props = next((p for p in child if isinstance(p.tag, str) and localname(p) == "Properties"), None)
if props is None:
continue
nm = next((n for n in props if isinstance(n.tag, str) and localname(n) == "Name"), None)
if nm is not None and nm.text:
names.add(nm.text.strip())
return names
def insert_into_own_child_objects(text, content):
"""Вставка в СОБСТВЕННЫЙ <ChildObjects> объекта. Свой контейнер закрывается в файле последним:
объект в файле один, а вложенные <ChildObjects> табличных частей закрываются раньше. Замена по
всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ."""
close_idx = text.rfind("</ChildObjects>")
if close_idx >= 0:
return text[:close_idx] + content + "\r\n\t\t" + text[close_idx:]
# Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже)
self_matches = list(re.finditer(r'<ChildObjects\s*/>', text))
if not self_matches:
return text
m = self_matches[-1]
return text[:m.start()] + f"<ChildObjects>{content}\r\n\t\t</ChildObjects>" + text[m.end():]
def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs
@@ -1203,10 +1247,8 @@ def main():
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read()
# Collect existing attribute names for dedup (text-based)
existing_names = set()
for m in re.finditer(r'<Name>(\w+)</Name>', obj_content):
existing_names.add(m.group(1))
# Collect existing names for dedup — только прямые дети своего ChildObjects
existing_names = get_own_child_object_names(obj_file)
all_attr_xml = ""
added = 0
@@ -1217,11 +1259,7 @@ def main():
added += 1
if added > 0:
# Insert attributes — handle both <ChildObjects/> and <ChildObjects>...</ChildObjects>
if re.search(r'<ChildObjects\s*/>', obj_content):
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
else:
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
obj_content = insert_into_own_child_objects(obj_content, all_attr_xml)
write_utf8_bom(obj_file, obj_content)
info(f" Merged {added} attribute(s) into: {obj_file}")
@@ -1265,11 +1303,7 @@ def main():
obj_content = fh.read()
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set()
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
existing_child_names = get_own_child_object_names(obj_file)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
@@ -1295,19 +1329,9 @@ def main():
if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content
# Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
if adopted_content:
# Handle <ChildObjects/> (self-closing)
if re.search(r'<ChildObjects\s*/>', obj_content):
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>\r\n{adopted_content}\r\n\t\t</ChildObjects>", obj_content)
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elif re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content):
m = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
existing_inner = m.group(1)
obj_content = obj_content.replace(
f"<ChildObjects>{existing_inner}</ChildObjects>",
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
)
obj_content = insert_into_own_child_objects(obj_content, f"\r\n{adopted_content}")
write_utf8_bom(obj_file, obj_content)
info(f" Enriched object: {obj_file}")