diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index eddd9afa..a9789188 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -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 } +# Имена ПРЯМЫХ детей собственного объекта — для дедупа при повторном +# заимствовании. Текстом это не снять: regex «первый до первого » +# у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и +# теряет то, что идёт после неё. +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 +} + +# Вставка в СОБСТВЕННЫЙ объекта. Свой контейнер закрывается в файле последним: +# объект в файле один, а вложенные табличных частей закрываются раньше. Замена по +# всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ. +function Insert-IntoOwnChildObjects { + param([string]$text, [string]$content) + + $closeIdx = $text.LastIndexOf('') + if ($closeIdx -ge 0) { + return $text.Substring(0, $closeIdx) + "${content}`r`n`t`t" + $text.Substring($closeIdx) + } + # Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже) + $selfMatches = [regex]::Matches($text, '') + if ($selfMatches.Count -eq 0) { return $text } + $m = $selfMatches[$selfMatches.Count - 1] + return $text.Substring(0, $m.Index) + "${content}`r`n`t`t" + $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 and .... - # Самозакрытый элемент раскрывается здесь же, а не пробельным узлом в DOM: тот давал + # Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал # лишнюю строку с табуляцией перед первым (у Конфигуратора пустых строк нет). - if ($text3 -match '') { - $text3 = [regex]::Replace($text3, '', "${allAttrXml}`r`n`t`t") - } else { - $text3 = $text3.Replace('', "${allAttrXml}`r`n`t`t") - } + $text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml # Пустой элемент: XmlWriter отдаёт ``, Конфигуратор пишет ``. Внутри # 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)(.*?)') { - foreach ($nm in [regex]::Matches($Matches[1], '(\w+)')) { - $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 объекта (там уже может лежать
) if ($adoptedContent) { - # Handle (self-closing) - if ($objContent -match '') { - $objContent = $objContent -replace '', "`r`n${adoptedContent}`r`n`t`t" - } - # Handle ... (may already have Form entry) - elseif ($objContent -match '(?s)(.*?)') { - $existingInner = $Matches[1] - $objContent = $objContent -replace '(?s)(.*?)', "${existingInner}`r`n${adoptedContent}`r`n`t`t" - } + $objContent = Insert-IntoOwnChildObjects $objContent "`r`n${adoptedContent}" } $encBom = New-Object System.Text.UTF8Encoding($true) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index dc193fcf..513eaa19 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -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): + """Имена ПРЯМЫХ детей собственного объекта — для дедупа при повторном + заимствовании. Текстом это не снять: regex «первый до первого » + у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и + теряет то, что идёт после неё.""" + 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): + """Вставка в СОБСТВЕННЫЙ объекта. Свой контейнер закрывается в файле последним: + объект в файле один, а вложенные табличных частей закрываются раньше. Замена по + всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ.""" + close_idx = text.rfind("") + if close_idx >= 0: + return text[:close_idx] + content + "\r\n\t\t" + text[close_idx:] + # Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже) + self_matches = list(re.finditer(r'', text)) + if not self_matches: + return text + m = self_matches[-1] + return text[:m.start()] + f"{content}\r\n\t\t" + 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'(\w+)', 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 and ... - if re.search(r'', obj_content): - obj_content = re.sub(r'', f"{all_attr_xml}\r\n\t\t", obj_content) - else: - obj_content = obj_content.replace("", f"{all_attr_xml}\r\n\t\t") + 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)(.*?)', obj_content) - if m_co: - for nm in re.findall(r'(\w+)', 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("", f"{props_xml}", 1) - # Replace empty ChildObjects with adopted content + # Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать ) if adopted_content: - # Handle (self-closing) - if re.search(r'', obj_content): - obj_content = re.sub(r'', f"\r\n{adopted_content}\r\n\t\t", obj_content) - # Handle ... (may already have Form entry) - elif re.search(r'(?s)(.*?)', obj_content): - m = re.search(r'(?s)(.*?)', obj_content) - existing_inner = m.group(1) - obj_content = obj_content.replace( - f"{existing_inner}", - f"{existing_inner}\r\n{adopted_content}\r\n\t\t" - ) + 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}")