mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-17 08:40:23 +03:00
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:
co-authored by
Claude Opus 5
parent
8c915df0d9
commit
b5662a380d
@@ -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
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -75,6 +75,50 @@ function Rewrite-ChoiceParameterLinks {
|
|||||||
return $xml
|
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 ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $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) }
|
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
|
||||||
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
|
||||||
# Insert attributes — handle both <ChildObjects/> and <ChildObjects>...</ChildObjects>.
|
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
|
||||||
# Самозакрытый элемент раскрывается здесь же, а не пробельным узлом в DOM: тот давал
|
|
||||||
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
||||||
if ($text3 -match '<ChildObjects\s*/>') {
|
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
|
||||||
$text3 = [regex]::Replace($text3, '<ChildObjects\s*/>', "<ChildObjects>${allAttrXml}`r`n`t`t</ChildObjects>")
|
|
||||||
} else {
|
|
||||||
$text3 = $text3.Replace('</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
@@ -1664,12 +1703,7 @@ function Borrow-MainAttribute {
|
|||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
$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)
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
$existingChildNames = @{}
|
$existingChildNames = Get-OwnChildObjectNames $objFile
|
||||||
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
|
||||||
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
|
||||||
$existingChildNames[$nm.Groups[1].Value] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
$insertTS = @($srcTS | 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) {
|
if ($adoptedContent) {
|
||||||
# Handle <ChildObjects/> (self-closing)
|
$objContent = Insert-IntoOwnChildObjects $objContent "`r`n${adoptedContent}"
|
||||||
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>"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -94,6 +94,50 @@ def rewrite_choice_parameter_links(xml, attr_uuids):
|
|||||||
return xml
|
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):
|
def decode_numeric_entities(s):
|
||||||
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||||
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
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:
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Collect existing attribute names for dedup (text-based)
|
# Collect existing names for dedup — только прямые дети своего ChildObjects
|
||||||
existing_names = set()
|
existing_names = get_own_child_object_names(obj_file)
|
||||||
for m in re.finditer(r'<Name>(\w+)</Name>', obj_content):
|
|
||||||
existing_names.add(m.group(1))
|
|
||||||
|
|
||||||
all_attr_xml = ""
|
all_attr_xml = ""
|
||||||
added = 0
|
added = 0
|
||||||
@@ -1217,11 +1259,7 @@ def main():
|
|||||||
added += 1
|
added += 1
|
||||||
|
|
||||||
if added > 0:
|
if added > 0:
|
||||||
# Insert attributes — handle both <ChildObjects/> and <ChildObjects>...</ChildObjects>
|
obj_content = insert_into_own_child_objects(obj_content, all_attr_xml)
|
||||||
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>")
|
|
||||||
write_utf8_bom(obj_file, obj_content)
|
write_utf8_bom(obj_file, obj_content)
|
||||||
info(f" Merged {added} attribute(s) into: {obj_file}")
|
info(f" Merged {added} attribute(s) into: {obj_file}")
|
||||||
|
|
||||||
@@ -1265,11 +1303,7 @@ def main():
|
|||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
existing_child_names = set()
|
existing_child_names = get_own_child_object_names(obj_file)
|
||||||
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)
|
|
||||||
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
|
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]
|
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:
|
if props_xml:
|
||||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
|
||||||
if adopted_content:
|
if adopted_content:
|
||||||
# Handle <ChildObjects/> (self-closing)
|
obj_content = insert_into_own_child_objects(obj_content, f"\r\n{adopted_content}")
|
||||||
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>"
|
|
||||||
)
|
|
||||||
|
|
||||||
write_utf8_bom(obj_file, obj_content)
|
write_utf8_bom(obj_file, obj_content)
|
||||||
info(f" Enriched object: {obj_file}")
|
info(f" Enriched object: {obj_file}")
|
||||||
|
|||||||
Reference in New Issue
Block a user