From c72f2210b52265be2296f80cb9aad714d8cf9a98 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 19:12:24 +0300 Subject: [PATCH 1/8] fix(cfe-borrow): strip DataPath, Events and preserve form properties in Borrow-Form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Borrowed forms failed to load with "Неверный путь к данным" and "Событие не было загружено" errors. Root cause: base form elements contained DataPath and Events referencing attributes and handlers not present in the extension. Changes: - Strip from base elements in both AutoCommandBar and ChildItems - Strip element-level from both sections - Collect form-level properties (AutoTitle, WindowOpeningMode, etc.) and write them into both main and BaseForm sections - Update 1c-extension-spec.md with rules 5-7 Co-Authored-By: Claude Opus 4.6 --- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 35 ++++++++++++++++--- .../skills/cfe-borrow/scripts/cfe-borrow.py | 25 ++++++++++++- docs/1c-extension-spec.md | 6 ++++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 0b526b35..89bbe6c6 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -476,13 +476,23 @@ function Borrow-Form { $formVersion = $srcFormEl.GetAttribute("version") if (-not $formVersion) { $formVersion = "2.17" } - # Find direct children: AutoCommandBar, ChildItems (visual elements only) + # Find direct children: form properties, AutoCommandBar, ChildItems $srcAutoCmd = $null $srcChildItems = $null + $formProps = @() + $reachedVisual = $false foreach ($fc in $srcFormEl.ChildNodes) { if ($fc.NodeType -ne 'Element') { continue } - if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { $srcAutoCmd = $fc } - elseif ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { $srcChildItems = $fc } + if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { + $reachedVisual = $true; $srcAutoCmd = $fc; continue + } + if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { + $reachedVisual = $true; $srcChildItems = $fc; continue + } + if (-not $reachedVisual) { + # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) + $formProps += $fc.OuterXml + } } # Get OuterXml and strip redundant namespace redeclarations (they're on root
) @@ -496,6 +506,10 @@ function Borrow-Form { $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') # Replace Autofill true → false $autoCmdXml = $autoCmdXml -replace 'true', 'false' + # Strip DataPath (references base form attributes not present in extension) + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') + # Strip element-level Events (base form event handlers not present in extension) + $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*.*?', '') } $childItemsXml = "" @@ -504,6 +518,9 @@ function Borrow-Form { $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') # Replace all CommandName values with 0 in ChildItems too $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') + # Strip DataPath and element-level Events + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') } else { $childItemsXml = "" } @@ -521,7 +538,11 @@ function Borrow-Form { $formXmlSb.Append($formTag) | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # Part 1: visual elements (add leading tab to first line of each block) + # Part 1: form properties + visual elements + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t$propXml`r`n") | Out-Null + } if ($autoCmdXml) { $formXmlSb.Append("`t$autoCmdXml") | Out-Null $formXmlSb.Append("`r`n") | Out-Null @@ -531,10 +552,14 @@ function Borrow-Form { $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # BaseForm: same visual elements, indented one more level + # BaseForm: form properties + same visual elements, indented one more level $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t`t$propXml`r`n") | Out-Null + } if ($autoCmdXml) { # Reindent for BaseForm: first line gets 2 tabs, other lines get +1 tab $acLines = $autoCmdXml -split "`r?`n" diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index 9b00d514..72c4e39c 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -653,14 +653,23 @@ def main(): src_auto_cmd = None src_child_items = None + form_props = [] + reached_visual = False for fc in src_form_el: if not isinstance(fc.tag, str): continue ln = localname(fc) if ln == "AutoCommandBar" and src_auto_cmd is None: + reached_visual = True src_auto_cmd = fc - elif ln == "ChildItems" and src_child_items is None: + continue + if ln == "ChildItems" and src_child_items is None: + reached_visual = True src_child_items = fc + continue + if not reached_visual: + # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) + form_props.append(etree.tostring(fc, encoding="unicode")) ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') @@ -670,12 +679,19 @@ def main(): auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) auto_cmd_xml = re.sub(r'[^<]*', '0', auto_cmd_xml) auto_cmd_xml = auto_cmd_xml.replace('true', 'false') + # Strip DataPath (references base form attributes not present in extension) + auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) + # Strip element-level Events (base form event handlers not present in extension) + auto_cmd_xml = re.sub(r'\s*.*?', '', auto_cmd_xml, flags=re.DOTALL) child_items_xml = "" if src_child_items is not None: child_items_xml = etree.tostring(src_child_items, encoding="unicode") child_items_xml = ns_strip_pattern.sub("", child_items_xml) child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) + # Strip DataPath and element-level Events + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*.*?', '', child_items_xml, flags=re.DOTALL) else: child_items_xml = "" @@ -696,6 +712,10 @@ def main(): parts.append(form_tag) parts.append("\r\n") + # Form properties (WindowOpeningMode, AutoFillCheck, etc.) + for prop_xml in form_props: + prop_xml_clean = ns_strip_pattern.sub("", prop_xml) + parts.append(f"\t{prop_xml_clean}\r\n") if auto_cmd_xml: parts.append(f"\t{auto_cmd_xml}\r\n") parts.append(f"\t{child_items_xml}\r\n") @@ -704,6 +724,9 @@ def main(): # BaseForm parts.append(f'\t\r\n') + for prop_xml in form_props: + prop_xml_clean = ns_strip_pattern.sub("", prop_xml) + parts.append(f"\t\t{prop_xml_clean}\r\n") if auto_cmd_xml: ac_lines = auto_cmd_xml.split("\n") for li, line in enumerate(ac_lines): diff --git a/docs/1c-extension-spec.md b/docs/1c-extension-spec.md index 465ee2f4..9208cca7 100644 --- a/docs/1c-extension-spec.md +++ b/docs/1c-extension-spec.md @@ -422,6 +422,12 @@ Form.xml заимствованной формы — **двухчастный ф 4. Элемент `` всегда идёт **последним** в `` и имеет атрибут `version`. +5. **Правило ``: удаление** — все элементы `...` из базовых визуальных элементов удаляются (и в Part 1, и в BaseForm). DataPath ссылается на реквизиты формы базовой конфигурации, которые не включены в расширение. Сохраняются только DataPath элементов, добавленных расширением (ссылающихся на собственные реквизиты расширения). + +6. **Правило `` элементов: удаление** — все блоки `` внутри визуальных элементов (AutoCommandBar и ChildItems) удаляются (и в Part 1, и в BaseForm). Обработчики событий базовой конфигурации не переносятся. При модификации формы обработчики расширения добавляются только в Part 1 с атрибутом `callType`. + +7. **Свойства формы** — элементы между `` и `` (например, `WindowOpeningMode`, `AutoFillCheck`, `AutoTitle`, `AutoTime`, `UsePostingMode`, `RepostOnWrite`, `Customizable`, `CommandBarLocation`) копируются из исходной формы в обе части (Part 1 и BaseForm). + #### 5.4.3. Нумерация ID элементов | Диапазон | Принадлежность | From cf5eae642846dcdb1f737e2eff4afbefc7e1bbf6 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 19:19:35 +0300 Subject: [PATCH 2/8] fix(cfe-borrow): generate minimal Form.xml without ChildItems for borrowed forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configurator generates borrowed forms with only properties + AutoCommandBar + empty Attributes — no ChildItems. ChildItems appear only when modifications are added via form-edit. Copying full ChildItems caused XDTO errors on complex forms due to unresolvable references (CommonPicture, StyleItem, xr: namespace elements). Changes: - Remove ChildItems extraction and output from Borrow-Form - Strip ChildItems from AutoCommandBar (keep only Autofill property) - Both main and BaseForm sections now contain only properties + minimal AutoCommandBar Co-Authored-By: Claude Opus 4.6 --- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 42 ++++--------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 89bbe6c6..7f36640a 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -476,9 +476,8 @@ function Borrow-Form { $formVersion = $srcFormEl.GetAttribute("version") if (-not $formVersion) { $formVersion = "2.17" } - # Find direct children: form properties, AutoCommandBar, ChildItems + # Find direct children: form properties and AutoCommandBar $srcAutoCmd = $null - $srcChildItems = $null $formProps = @() $reachedVisual = $false foreach ($fc in $srcFormEl.ChildNodes) { @@ -486,8 +485,8 @@ function Borrow-Form { if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { $reachedVisual = $true; $srcAutoCmd = $fc; continue } - if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { - $reachedVisual = $true; $srcChildItems = $fc; continue + if ($fc.LocalName -eq 'ChildItems' -or $fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters') { + $reachedVisual = $true; continue } if (-not $reachedVisual) { # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) @@ -498,31 +497,15 @@ function Borrow-Form { # Get OuterXml and strip redundant namespace redeclarations (they're on root ) $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' + # AutoCommandBar: copy only properties (Autofill etc.), strip ChildItems $autoCmdXml = "" if ($srcAutoCmd) { $autoCmdXml = $srcAutoCmd.OuterXml $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') - # Replace all CommandName values with 0 (base form buttons lose command refs) - $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') + # Strip ChildItems from AutoCommandBar (buttons will appear from base form at runtime) + $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*.*?', '') # Replace Autofill true → false $autoCmdXml = $autoCmdXml -replace 'true', 'false' - # Strip DataPath (references base form attributes not present in extension) - $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') - # Strip element-level Events (base form event handlers not present in extension) - $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*.*?', '') - } - - $childItemsXml = "" - if ($srcChildItems) { - $childItemsXml = $srcChildItems.OuterXml - $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') - # Replace all CommandName values with 0 in ChildItems too - $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') - # Strip DataPath and element-level Events - $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') - $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') - } else { - $childItemsXml = "" } # Extract the opening tag from source text (preserves namespace declarations) @@ -538,7 +521,7 @@ function Borrow-Form { $formXmlSb.Append($formTag) | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # Part 1: form properties + visual elements + # Part 1: form properties + AutoCommandBar (no ChildItems — they come from base form at runtime) foreach ($propXml in $formProps) { $propXml = [regex]::Replace($propXml, $nsStripPattern, '') $formXmlSb.Append("`t$propXml`r`n") | Out-Null @@ -547,12 +530,10 @@ function Borrow-Form { $formXmlSb.Append("`t$autoCmdXml") | Out-Null $formXmlSb.Append("`r`n") | Out-Null } - $formXmlSb.Append("`t$childItemsXml") | Out-Null - $formXmlSb.Append("`r`n") | Out-Null $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # BaseForm: form properties + same visual elements, indented one more level + # BaseForm: same properties + AutoCommandBar (no ChildItems) $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null @@ -570,13 +551,6 @@ function Borrow-Form { } } - $ciLines = $childItemsXml -split "`r?`n" - for ($li = 0; $li -lt $ciLines.Count; $li++) { - if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } - else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } - $formXmlSb.Append("`r`n") | Out-Null - } - $formXmlSb.Append("`t`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null $formXmlSb.Append("`t") | Out-Null From 007b4ec69c55700a31426ce0074df4b994e2b5a3 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 19:20:36 +0300 Subject: [PATCH 3/8] fix(cfe-borrow): mirror minimal Form.xml changes to Python port Co-Authored-By: Claude Opus 4.6 --- .../skills/cfe-borrow/scripts/cfe-borrow.py | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index 72c4e39c..04c828f0 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -652,7 +652,6 @@ def main(): form_version = src_form_el.get("version", "2.17") src_auto_cmd = None - src_child_items = None form_props = [] reached_visual = False for fc in src_form_el: @@ -663,9 +662,8 @@ def main(): reached_visual = True src_auto_cmd = fc continue - if ln == "ChildItems" and src_child_items is None: + if ln in ("ChildItems", "Events", "Attributes", "Commands", "Parameters"): reached_visual = True - src_child_items = fc continue if not reached_visual: # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) @@ -673,27 +671,15 @@ def main(): ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + # AutoCommandBar: copy only properties (Autofill etc.), strip ChildItems auto_cmd_xml = "" if src_auto_cmd is not None: auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) - auto_cmd_xml = re.sub(r'[^<]*', '0', auto_cmd_xml) + # Strip ChildItems from AutoCommandBar (buttons will appear from base form at runtime) + auto_cmd_xml = re.sub(r'\s*.*?', '', auto_cmd_xml, flags=re.DOTALL) + # Replace Autofill true -> false auto_cmd_xml = auto_cmd_xml.replace('true', 'false') - # Strip DataPath (references base form attributes not present in extension) - auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) - # Strip element-level Events (base form event handlers not present in extension) - auto_cmd_xml = re.sub(r'\s*.*?', '', auto_cmd_xml, flags=re.DOTALL) - - child_items_xml = "" - if src_child_items is not None: - child_items_xml = etree.tostring(src_child_items, encoding="unicode") - child_items_xml = ns_strip_pattern.sub("", child_items_xml) - child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) - # Strip DataPath and element-level Events - child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) - child_items_xml = re.sub(r'\s*.*?', '', child_items_xml, flags=re.DOTALL) - else: - child_items_xml = "" # Extract source form opening tag xml_decl = '' @@ -712,16 +698,15 @@ def main(): parts.append(form_tag) parts.append("\r\n") - # Form properties (WindowOpeningMode, AutoFillCheck, etc.) + # Part 1: form properties + AutoCommandBar (no ChildItems — they come from base form at runtime) for prop_xml in form_props: prop_xml_clean = ns_strip_pattern.sub("", prop_xml) parts.append(f"\t{prop_xml_clean}\r\n") if auto_cmd_xml: parts.append(f"\t{auto_cmd_xml}\r\n") - parts.append(f"\t{child_items_xml}\r\n") parts.append("\t\r\n") - # BaseForm + # BaseForm: same properties + AutoCommandBar (no ChildItems) parts.append(f'\t\r\n') for prop_xml in form_props: @@ -736,14 +721,6 @@ def main(): parts.append(f"\t{line}") parts.append("\r\n") - ci_lines = child_items_xml.split("\n") - for li, line in enumerate(ci_lines): - if li == 0: - parts.append(f"\t\t{line}") - else: - parts.append(f"\t{line}") - parts.append("\r\n") - parts.append("\t\t\r\n") parts.append("\t\r\n") parts.append("") From 6df64ae1c163eddf1f1ce4877223e5e1d316d712 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 20:18:12 +0300 Subject: [PATCH 4/8] feat(cfe-borrow): full ChildItems form borrowing with auto-borrow dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Borrow-Form now generates full ChildItems tree (matching Configurator output) with stripping of DataPath, TitleDataPath, TypeLink, Events, CommandName→0. Auto-borrows CommonPictures, StyleItems, and Enums+EnumValues referenced by form elements. Verified loading into BP database with two production forms. Co-Authored-By: Claude Opus 4.6 --- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 205 +++++++++++++++++- .../skills/cfe-borrow/scripts/cfe-borrow.py | 166 +++++++++++++- docs/1c-extension-spec.md | 109 +++++----- 3 files changed, 412 insertions(+), 68 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 7f36640a..a64ec15b 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.0 — Borrow objects from configuration into extension (CFE) +# cfe-borrow v1.1 — Borrow objects from configuration into extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][string]$ExtensionPath, @@ -476,8 +476,9 @@ function Borrow-Form { $formVersion = $srcFormEl.GetAttribute("version") if (-not $formVersion) { $formVersion = "2.17" } - # Find direct children: form properties and AutoCommandBar + # Find direct children: form properties, AutoCommandBar, ChildItems $srcAutoCmd = $null + $srcChildItems = $null $formProps = @() $reachedVisual = $false foreach ($fc in $srcFormEl.ChildNodes) { @@ -485,11 +486,13 @@ function Borrow-Form { if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { $reachedVisual = $true; $srcAutoCmd = $fc; continue } - if ($fc.LocalName -eq 'ChildItems' -or $fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters') { + if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { + $reachedVisual = $true; $srcChildItems = $fc; continue + } + if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters') { $reachedVisual = $true; continue } if (-not $reachedVisual) { - # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) $formProps += $fc.OuterXml } } @@ -497,17 +500,187 @@ function Borrow-Form { # Get OuterXml and strip redundant namespace redeclarations (they're on root
) $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' - # AutoCommandBar: copy only properties (Autofill etc.), strip ChildItems + # AutoCommandBar: strip ChildItems (buttons), replace CommandName→0, Autofill→false $autoCmdXml = "" if ($srcAutoCmd) { $autoCmdXml = $srcAutoCmd.OuterXml $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') - # Strip ChildItems from AutoCommandBar (buttons will appear from base form at runtime) $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*.*?', '') - # Replace Autofill true → false $autoCmdXml = $autoCmdXml -replace 'true', 'false' } + # ChildItems: copy full tree, clean up base-config references + $childItemsXml = "" + if ($srcChildItems) { + $childItemsXml = $srcChildItems.OuterXml + $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') + # Replace all CommandName values with 0 + $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') + # Strip DataPath (references base form attributes not in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip TitleDataPath (e.g. Объект.Товары.RowsCount — invalid without base attributes) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*Items\.[^<]*.*?', '') + # Strip element-level Events (base form handlers not in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') + + # Collect CommonPicture references from ChildItems + $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') + $referencedPictures = @{} + foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true } + + # Auto-borrow referenced CommonPictures (if not already borrowed) + $autoBorrowedPics = @() + foreach ($picName in $referencedPictures.Keys) { + if (-not (Test-ObjectBorrowed "CommonPicture" $picName)) { + $picSrcFile = Join-Path (Join-Path $cfgDir "CommonPictures") "${picName}.xml" + if (Test-Path $picSrcFile) { + $src = Read-SourceObject "CommonPicture" $picName + $borrowedXml = Build-BorrowedObjectXml "CommonPicture" $picName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "CommonPictures" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${picName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "CommonPicture" $picName + $autoBorrowedPics += $picName + $borrowedFiles += $targetFile + Info " Auto-borrowed: CommonPicture.${picName}" + } else { + Warn " CommonPicture.${picName} not found in source config — will strip from form" + } + } + } + + # Collect all borrowed CommonPictures (including previously borrowed) + $borrowedPicSet = @{} + $nsMgr2 = New-Object System.Xml.XmlNamespaceManager($script:xmlDoc.NameTable) + $nsMgr2.AddNamespace("md", $script:mdNs) + $picNodes = $script:xmlDoc.SelectNodes("//md:ChildObjects/md:CommonPicture", $nsMgr2) + foreach ($pn in $picNodes) { $borrowedPicSet[$pn.InnerText] = $true } + + # Strip blocks referencing non-borrowed CommonPictures + $picBlockPattern = '(?s)\s*\s*CommonPicture\.(\w+).*?' + $picMatches = [regex]::Matches($childItemsXml, $picBlockPattern) + # Process in reverse order to preserve positions + for ($mi = $picMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $picMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $childItemsXml = $childItemsXml.Remove($pm.Index, $pm.Length) + } + } + # Strip StdPicture blocks (except Print) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + + # Auto-borrow StyleItems referenced in ChildItems + # Pattern 1: , + # Pattern 2: style:XXX, style:XXX, etc. + $referencedStyles = @{} + $styleRefs1 = [regex]::Matches($childItemsXml, 'ref="style:(\w+)"[^>]*kind="StyleItem"') + foreach ($m in $styleRefs1) { $referencedStyles[$m.Groups[1].Value] = $true } + $styleRefs2 = [regex]::Matches($childItemsXml, '>style:(\w+)') + foreach ($m in $styleRefs2) { $referencedStyles[$m.Groups[1].Value] = $true } + + foreach ($styleName in $referencedStyles.Keys) { + if (-not (Test-ObjectBorrowed "StyleItem" $styleName)) { + $styleSrcFile = Join-Path (Join-Path $cfgDir "StyleItems") "${styleName}.xml" + if (Test-Path $styleSrcFile) { + $src = Read-SourceObject "StyleItem" $styleName + $borrowedXml = Build-BorrowedObjectXml "StyleItem" $styleName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "StyleItems" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${styleName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "StyleItem" $styleName + $borrowedFiles += $targetFile + Info " Auto-borrowed: StyleItem.${styleName}" + } else { + Warn " StyleItem.${styleName} not found in source config" + } + } + } + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef in ChoiceParameters + # Collect Enum -> [EnumValue names] map + $dtRefs = [regex]::Matches($childItemsXml, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)') + $referencedEnumValues = @{} + foreach ($m in $dtRefs) { + $eName = $m.Groups[1].Value + $evName = $m.Groups[2].Value + if (-not $referencedEnumValues.ContainsKey($eName)) { $referencedEnumValues[$eName] = @{} } + $referencedEnumValues[$eName][$evName] = $true + } + + foreach ($enumName in $referencedEnumValues.Keys) { + if (-not (Test-ObjectBorrowed "Enum" $enumName)) { + $enumSrcFile = Join-Path (Join-Path $cfgDir "Enums") "${enumName}.xml" + if (Test-Path $enumSrcFile) { + # Read source Enum to get UUID and EnumValue UUIDs + $srcParser = New-Object System.Xml.XmlDocument + $srcParser.PreserveWhitespace = $true + $srcParser.Load($enumSrcFile) + $srcEnumEl = $null + foreach ($cn in $srcParser.DocumentElement.ChildNodes) { + if ($cn.NodeType -eq 'Element') { $srcEnumEl = $cn; break } + } + $srcEnumUuid = $srcEnumEl.GetAttribute("uuid") + + # Find source EnumValues by name + $enumValueXmls = @() + $neededValues = $referencedEnumValues[$enumName] + $srcNsMgr = New-Object System.Xml.XmlNamespaceManager($srcParser.NameTable) + $srcNsMgr.AddNamespace("md", $script:mdNs) + $srcEvNodes = $srcEnumEl.SelectNodes("md:ChildObjects/md:EnumValue", $srcNsMgr) + foreach ($evNode in $srcEvNodes) { + $evUuid = $evNode.GetAttribute("uuid") + $evNameNode = $evNode.SelectSingleNode("md:Properties/md:Name", $srcNsMgr) + if ($evNameNode -and $neededValues.ContainsKey($evNameNode.InnerText)) { + $newEvUuid = [guid]::NewGuid().ToString() + $enumValueXmls += @" + + + + Adopted + $($evNameNode.InnerText) + + ${evUuid} + + +"@ + } + } + + # Build borrowed Enum with EnumValues in ChildObjects + $src = Read-SourceObject "Enum" $enumName + $borrowedXml = Build-BorrowedObjectXml "Enum" $enumName $src.Uuid $src.Properties + if ($enumValueXmls.Count -gt 0) { + $evBlock = ($enumValueXmls -join "`r`n") + $borrowedXml = $borrowedXml -replace '', "`r`n${evBlock}`r`n`t`t" + } + + $targetDir = Join-Path $extDir "Enums" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${enumName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "Enum" $enumName + $borrowedFiles += $targetFile + Info " Auto-borrowed: Enum.${enumName} (with $($enumValueXmls.Count) EnumValue(s))" + } else { + Warn " Enum.${enumName} not found in source config" + } + } + } + } + # Extract the opening tag from source text (preserves namespace declarations) $xmlDecl = '' $formTag = "" @@ -521,7 +694,7 @@ function Borrow-Form { $formXmlSb.Append($formTag) | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # Part 1: form properties + AutoCommandBar (no ChildItems — they come from base form at runtime) + # Part 1: form properties + AutoCommandBar + ChildItems foreach ($propXml in $formProps) { $propXml = [regex]::Replace($propXml, $nsStripPattern, '') $formXmlSb.Append("`t$propXml`r`n") | Out-Null @@ -530,10 +703,14 @@ function Borrow-Form { $formXmlSb.Append("`t$autoCmdXml") | Out-Null $formXmlSb.Append("`r`n") | Out-Null } + if ($childItemsXml) { + $formXmlSb.Append("`t$childItemsXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # BaseForm: same properties + AutoCommandBar (no ChildItems) + # BaseForm: same content, indented one more level $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null @@ -542,7 +719,6 @@ function Borrow-Form { $formXmlSb.Append("`t`t$propXml`r`n") | Out-Null } if ($autoCmdXml) { - # Reindent for BaseForm: first line gets 2 tabs, other lines get +1 tab $acLines = $autoCmdXml -split "`r?`n" for ($li = 0; $li -lt $acLines.Count; $li++) { if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } @@ -550,6 +726,15 @@ function Borrow-Form { $formXmlSb.Append("`r`n") | Out-Null } } + if ($childItemsXml) { + # Reindent ChildItems for BaseForm (+1 tab level) + $ciLines = $childItemsXml -split "`r?`n" + for ($li = 0; $li -lt $ciLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + } $formXmlSb.Append("`t`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index 04c828f0..9db65bef 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.0 — Borrow objects from configuration into extension (CFE) +# cfe-borrow v1.1 — Borrow objects from configuration into extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -671,17 +671,159 @@ def main(): ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') - # AutoCommandBar: copy only properties (Autofill etc.), strip ChildItems + # AutoCommandBar: strip ChildItems (buttons), replace CommandName→0, Autofill→false auto_cmd_xml = "" if src_auto_cmd is not None: auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) - # Strip ChildItems from AutoCommandBar (buttons will appear from base form at runtime) auto_cmd_xml = re.sub(r'\s*.*?', '', auto_cmd_xml, flags=re.DOTALL) - # Replace Autofill true -> false auto_cmd_xml = auto_cmd_xml.replace('true', 'false') - # Extract source form opening tag + # ChildItems: copy full tree, clean up base-config references + child_items_xml = "" + src_child_items = None + for fc in src_form_el: + if isinstance(fc.tag, str) and localname(fc) == "ChildItems": + src_child_items = fc + break + + if src_child_items is not None: + child_items_xml = etree.tostring(src_child_items, encoding="unicode") + child_items_xml = ns_strip_pattern.sub("", child_items_xml) + # Replace all CommandName values with 0 + child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) + # Strip DataPath + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip TitleDataPath + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip TypeLink blocks with human-readable DataPath (Items.XXX) + child_items_xml = re.sub(r'\s*\s*Items\.[^<]*.*?', '', child_items_xml, flags=re.DOTALL) + # Strip element-level Events + child_items_xml = re.sub(r'\s*.*?', '', child_items_xml, flags=re.DOTALL) + + # Auto-borrow referenced CommonPictures + pic_refs = re.findall(r'CommonPicture\.(\w+)', child_items_xml) + referenced_pictures = {name: True for name in pic_refs} + + auto_borrowed_pics = [] + for pic_name in referenced_pictures: + if not test_object_borrowed("CommonPicture", pic_name): + pic_src_file = os.path.join(cfg_dir, "CommonPictures", f"{pic_name}.xml") + if os.path.isfile(pic_src_file): + src = read_source_object("CommonPicture", pic_name) + borrowed_xml = build_borrowed_object_xml("CommonPicture", pic_name, src["uuid"], src["properties"]) + target_dir = os.path.join(ext_dir, "CommonPictures") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{pic_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("CommonPicture", pic_name) + auto_borrowed_pics.append(pic_name) + info(f" Auto-borrowed: CommonPicture.{pic_name}") + else: + warn(f" CommonPicture.{pic_name} not found in source config — will strip from form") + + # Collect all borrowed CommonPictures for Picture stripping + borrowed_pic_set = set() + for co_child in child_objs_el: + if isinstance(co_child.tag, str) and localname(co_child) == "CommonPicture": + borrowed_pic_set.add((co_child.text or "").strip()) + + # Strip blocks referencing non-borrowed CommonPictures (reverse order) + pic_block_pattern = re.compile(r'\s*\s*CommonPicture\.(\w+).*?', re.DOTALL) + pic_matches = list(pic_block_pattern.finditer(child_items_xml)) + for pm in reversed(pic_matches): + cp_name = pm.group(1) + if cp_name not in borrowed_pic_set: + child_items_xml = child_items_xml[:pm.start()] + child_items_xml[pm.end():] + # Strip StdPicture blocks (except Print) + child_items_xml = re.sub(r'\s*\s*StdPicture\.(?!Print\b)\w+.*?', '', child_items_xml, flags=re.DOTALL) + + # Auto-borrow StyleItems referenced in ChildItems + referenced_styles = set() + for m in re.finditer(r'ref="style:(\w+)"[^>]*kind="StyleItem"', child_items_xml): + referenced_styles.add(m.group(1)) + for m in re.finditer(r'>style:(\w+)', child_items_xml): + referenced_styles.add(m.group(1)) + + for style_name in referenced_styles: + if not test_object_borrowed("StyleItem", style_name): + style_src_file = os.path.join(cfg_dir, "StyleItems", f"{style_name}.xml") + if os.path.isfile(style_src_file): + src = read_source_object("StyleItem", style_name) + borrowed_xml = build_borrowed_object_xml("StyleItem", style_name, src["uuid"], src["properties"]) + target_dir = os.path.join(ext_dir, "StyleItems") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{style_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("StyleItem", style_name) + info(f" Auto-borrowed: StyleItem.{style_name}") + else: + warn(f" StyleItem.{style_name} not found in source config") + + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef + referenced_enum_values = {} # enum_name -> set of value_names + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', child_items_xml): + e_name, ev_name = m.group(1), m.group(2) + if e_name not in referenced_enum_values: + referenced_enum_values[e_name] = set() + referenced_enum_values[e_name].add(ev_name) + + for enum_name, needed_values in referenced_enum_values.items(): + if not test_object_borrowed("Enum", enum_name): + enum_src_file = os.path.join(cfg_dir, "Enums", f"{enum_name}.xml") + if os.path.isfile(enum_src_file): + # Read source Enum to find EnumValue UUIDs + src_enum_tree = etree.parse(enum_src_file, etree.XMLParser(remove_blank_text=False)) + src_enum_root = src_enum_tree.getroot() + src_enum_el = None + for cn in src_enum_root: + if isinstance(cn.tag, str): + src_enum_el = cn + break + + # Find needed EnumValues + ev_xmls = [] + for ev_node in src_enum_el.iter(): + if isinstance(ev_node.tag, str) and localname(ev_node) == "EnumValue": + ev_uuid = ev_node.get("uuid", "") + name_el = None + for props in ev_node: + if isinstance(props.tag, str) and localname(props) == "Properties": + for prop in props: + if isinstance(prop.tag, str) and localname(prop) == "Name": + name_el = prop + break + if name_el is not None and (name_el.text or "").strip() in needed_values: + new_ev_uuid = str(uuid.uuid4()) + ev_xmls.append( + f'\t\t\t\n' + f'\t\t\t\t\n' + f'\t\t\t\t\n' + f'\t\t\t\t\tAdopted\n' + f'\t\t\t\t\t{name_el.text.strip()}\n' + f'\t\t\t\t\t\n' + f'\t\t\t\t\t{ev_uuid}\n' + f'\t\t\t\t\n' + f'\t\t\t' + ) + + # Build borrowed Enum with EnumValues + src_obj = read_source_object("Enum", enum_name) + borrowed_xml = build_borrowed_object_xml("Enum", enum_name, src_obj["uuid"], src_obj["properties"]) + if ev_xmls: + ev_block = "\n".join(ev_xmls) + borrowed_xml = borrowed_xml.replace("", f"\n{ev_block}\n\t\t") + + target_dir = os.path.join(ext_dir, "Enums") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{enum_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("Enum", enum_name) + info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))") + else: + warn(f" Enum.{enum_name} not found in source config") + + # Extract the opening tag from source text xml_decl = '' form_tag = f'' m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content) @@ -698,15 +840,17 @@ def main(): parts.append(form_tag) parts.append("\r\n") - # Part 1: form properties + AutoCommandBar (no ChildItems — they come from base form at runtime) + # Part 1: form properties + AutoCommandBar + ChildItems for prop_xml in form_props: prop_xml_clean = ns_strip_pattern.sub("", prop_xml) parts.append(f"\t{prop_xml_clean}\r\n") if auto_cmd_xml: parts.append(f"\t{auto_cmd_xml}\r\n") + if child_items_xml: + parts.append(f"\t{child_items_xml}\r\n") parts.append("\t\r\n") - # BaseForm: same properties + AutoCommandBar (no ChildItems) + # BaseForm: same content, indented one more level parts.append(f'\t\r\n') for prop_xml in form_props: @@ -720,6 +864,14 @@ def main(): else: parts.append(f"\t{line}") parts.append("\r\n") + if child_items_xml: + ci_lines = child_items_xml.split("\n") + for li, line in enumerate(ci_lines): + if li == 0: + parts.append(f"\t\t{line}") + else: + parts.append(f"\t{line}") + parts.append("\r\n") parts.append("\t\t\r\n") parts.append("\t\r\n") diff --git a/docs/1c-extension-spec.md b/docs/1c-extension-spec.md index 9208cca7..aaa2cfb2 100644 --- a/docs/1c-extension-spec.md +++ b/docs/1c-extension-spec.md @@ -356,77 +356,84 @@ Enums/ # Перечисления #### 5.4.2. Структура Form.xml заимствованной формы -Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Обе части содержат **только визуальные элементы** — атрибуты, события, параметры и команды базовой конфигурации **НЕ включаются**. +Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Существуют **два варианта** в зависимости от наличия модификаций модуля формы. + +##### Вариант A — Минимальная форма (чистое заимствование) + +Когда форма заимствована без модификации модуля — Form.xml содержит **только свойства формы**, AutoCommandBar без кнопок и пустые Attributes. **Нет ChildItems**. Обе секции (Part 1 и BaseForm) идентичны. ```xml - - + false + CurrentOrLast + - - - - - - + false - - - - - - - Расш1_ПриСозданииПосле - - - - - Расш1_НоваяКомандаВместо - - + + - + false + CurrentOrLast + - - - - + false - - - - - + - ``` -**Ключевые правила:** +##### Вариант B — Полная форма (заимствование процедуры модуля через `ИзменениеИКонтроль`) -1. **Часть 1** (до ``) — **результирующая форма**. Содержит визуальные элементы (AutoCommandBar + ChildItems) из базовой конфигурации плюс элементы расширения. Атрибуты базовой конфигурации (DynamicList, QueryText и др.) **не включаются** — только реквизиты расширения (id ≥ 1000000) или пустой ``. Events и Commands — только добавленные расширением (с `callType`). +Когда в расширении заимствуется процедура из модуля формы, Конфигуратор выгружает **полное дерево ChildItems** с применением правил очистки. -2. **Часть 2** (``) — **визуальный снимок исходной формы**. Содержит только AutoCommandBar + ChildItems + пустой ``. НЕ содержит Events, Commands, Parameters. Все `` в кнопках заменены на `0`. Платформа использует BaseForm для контроля совместимости при обновлении конфигурации. +```xml +
+ false + + + false + + + + + + + -3. **Правило `0`**: во всех кнопках базовой формы (как в Part 1, так и в BaseForm) значение `` заменяется на `0`. Ссылки на команды конфигурации не сохраняются. Только кнопки, добавленные расширением, сохраняют ссылку на команду (напр. `Form.Command.XXX`). + + + + +``` -4. Элемент `` всегда идёт **последним** в `
` и имеет атрибут `version`. +**Ключевые правила (для обоих вариантов):** -5. **Правило ``: удаление** — все элементы `...` из базовых визуальных элементов удаляются (и в Part 1, и в BaseForm). DataPath ссылается на реквизиты формы базовой конфигурации, которые не включены в расширение. Сохраняются только DataPath элементов, добавленных расширением (ссылающихся на собственные реквизиты расширения). +1. **Свойства формы** — элементы между `` и `` (напр. `AutoTitle`, `AutoTime`, `UsePostingMode`, `RepostOnWrite`, `WindowOpeningMode`, `Customizable`, `CommandBarLocation`) копируются из исходной формы в обе секции. -6. **Правило `` элементов: удаление** — все блоки `` внутри визуальных элементов (AutoCommandBar и ChildItems) удаляются (и в Part 1, и в BaseForm). Обработчики событий базовой конфигурации не переносятся. При модификации формы обработчики расширения добавляются только в Part 1 с атрибутом `callType`. +2. **AutoCommandBar** — присутствует всегда с `id="-1"`, но без `` (кнопки удаляются). `` = `false`. -7. **Свойства формы** — элементы между `` и `` (например, `WindowOpeningMode`, `AutoFillCheck`, `AutoTitle`, `AutoTime`, `UsePostingMode`, `RepostOnWrite`, `Customizable`, `CommandBarLocation`) копируются из исходной формы в обе части (Part 1 и BaseForm). +3. **Attributes** — пустой `` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1. + +4. **BaseForm** — последний элемент в ``, атрибут `version`. В BaseForm **нет** Events, Commands, Parameters. + +5. **DataPath: удаление** (вариант B) — все `` из базовых элементов удаляются в обеих секциях. DataPath ссылается на реквизиты, не включённые в расширение. + +6. **TitleDataPath: удаление** (вариант B) — все `` удаляются (напр. `Объект.Товары.RowsCount` — путь недействителен без базовых атрибутов). + +7. **TypeLink: удаление** (вариант B) — блоки `` с `Items.*` удаляются (человекочитаемые пути, которые нельзя преобразовать в UUID-формат Конфигуратора). + +8. **Events элементов: удаление** (вариант B) — все `` внутри визуальных элементов удаляются в обеих секциях. Обработчики расширения добавляются через `elementEvents` в Part 1 с `callType`. + +9. **Picture stripping** (вариант B) — блоки `` с `CommonPicture.XXX` удаляются, если `CommonPicture.XXX` **не заимствован** в расширение. Сам элемент PictureDecoration остаётся, только `` убирается. `StdPicture.Print` сохраняется, остальные StdPicture удаляются. + +10. **Авто-заимствование CommonPictures** — при заимствовании формы автоматически заимствуются все CommonPictures, на которые ссылаются элементы формы. + +11. **Авто-заимствование StyleItems** — элементы формы ссылаются на StyleItems через `` и `style:XXX`. Все такие StyleItems должны быть заимствованы. Стандартные стили (NormalTextFont, AccentColor, FormBackColor и др.) не имеют файлов и автоматически пропускаются. + +12. **Авто-заимствование Enums + EnumValues** — `` могут содержать `Enum.XXX.EnumValue.YYY`. Перечисление `Enum.XXX` заимствуется вместе с конкретными `EnumValue` (borrowed с `ExtendedConfigurationObject` указывающим на UUID оригинального значения). #### 5.4.3. Нумерация ID элементов From b5a779bd5de5318117c8a62d40f912263c092523 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 20:23:34 +0300 Subject: [PATCH 5/8] fix(cfe-borrow): fix dict key casing in Python auto-borrow (Uuid/Properties) read_source_object returns keys with capital letters (Uuid, Properties), but auto-borrow sections for CommonPicture, StyleItem, and Enum used lowercase. E2E tested: Python port loads into BP_DEMO successfully. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/cfe-borrow/scripts/cfe-borrow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index 9db65bef..dca7c817 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -711,7 +711,7 @@ def main(): pic_src_file = os.path.join(cfg_dir, "CommonPictures", f"{pic_name}.xml") if os.path.isfile(pic_src_file): src = read_source_object("CommonPicture", pic_name) - borrowed_xml = build_borrowed_object_xml("CommonPicture", pic_name, src["uuid"], src["properties"]) + borrowed_xml = build_borrowed_object_xml("CommonPicture", pic_name, src["Uuid"], src["Properties"]) target_dir = os.path.join(ext_dir, "CommonPictures") os.makedirs(target_dir, exist_ok=True) target_file = os.path.join(target_dir, f"{pic_name}.xml") @@ -750,7 +750,7 @@ def main(): style_src_file = os.path.join(cfg_dir, "StyleItems", f"{style_name}.xml") if os.path.isfile(style_src_file): src = read_source_object("StyleItem", style_name) - borrowed_xml = build_borrowed_object_xml("StyleItem", style_name, src["uuid"], src["properties"]) + borrowed_xml = build_borrowed_object_xml("StyleItem", style_name, src["Uuid"], src["Properties"]) target_dir = os.path.join(ext_dir, "StyleItems") os.makedirs(target_dir, exist_ok=True) target_file = os.path.join(target_dir, f"{style_name}.xml") @@ -809,7 +809,7 @@ def main(): # Build borrowed Enum with EnumValues src_obj = read_source_object("Enum", enum_name) - borrowed_xml = build_borrowed_object_xml("Enum", enum_name, src_obj["uuid"], src_obj["properties"]) + borrowed_xml = build_borrowed_object_xml("Enum", enum_name, src_obj["Uuid"], src_obj["Properties"]) if ev_xmls: ev_block = "\n".join(ev_xmls) borrowed_xml = borrowed_xml.replace("", f"\n{ev_block}\n\t\t") From e7e6c885c74ac773cc9c915e37ea3da26638c81f Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 20:58:17 +0300 Subject: [PATCH 6/8] feat(cfe-validate): add checks 10-13 for deep extension validation Add sub-item validation (Attribute, TabularSection, EnumValue, Form), borrowed form structure checks, form dependency analysis (CommonPicture, StyleItem with platform whitelist, Enum DesignTimeRef), and TypeLink validation. Fix DataPath false positive by scoping check to BaseForm content only. Both PS1 and Python ports updated to v1.2. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/cfe-validate/SKILL.md | 6 +- .../cfe-validate/scripts/cfe-validate.ps1 | 337 +++++++++++++++++- .../cfe-validate/scripts/cfe-validate.py | 301 +++++++++++++++- 3 files changed, 621 insertions(+), 23 deletions(-) diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index 9bebc2e0..c4e013eb 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -28,7 +28,7 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml" ``` -## Проверки (9 шагов) +## Проверки (13 шагов) | # | Проверка | Уровень | |---|----------|---------| @@ -41,5 +41,9 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate | 7 | Файлы языков существуют | WARN | | 8 | Каталоги объектов существуют | WARN | | 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN | +| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR | +| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm (DataPath, TitleDataPath) | ERROR/WARN | +| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN | +| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN | Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации. diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index 76c44101..0c665f84 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -1,4 +1,4 @@ -# cfe-validate v1.1 — Validate 1C configuration extension structure (CFE) +# cfe-validate v1.2 — Validate 1C configuration extension structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -389,7 +389,7 @@ if (-not $childObjNode) { } else { $check5Ok = $true $totalCount = 0 - $typeCounts = @{} + $script:childObjectIndex = @{} $duplicates = @{} $typeFirstIndex = @{} $lastTypeOrder = -1 @@ -415,21 +415,21 @@ if (-not $childObjNode) { } } - if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } - if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} } + if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) { if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { Report-Error "5. Duplicate: $typeName.$objNameVal" $duplicates["$typeName.$objNameVal"] = $true $check5Ok = $false } } else { - $typeCounts[$typeName][$objNameVal] = $true + $script:childObjectIndex[$typeName][$objNameVal] = $true } $totalCount++ } - $typeCount = $typeCounts.Count + $typeCount = $script:childObjectIndex.Count if ($check5Ok) { $orderInfo = if ($orderOk) { ", order correct" } else { "" } Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" @@ -533,11 +533,46 @@ if ($childObjNode) { if ($script:stopped) { & $finalize; exit 1 } -# --- Check 9: Borrowed objects validation --- +# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- +$script:enumValuesIndex = @{} +$script:formList = @() + +# Helper: validate a borrowed Attribute/EnumValue sub-item +function Validate-BorrowedSubItem { + param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { + Report-Error "${checkNum}. ${context}: ${subType} missing Properties" + return $false + } + $ok = $true + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if (-not $subOb -or $subOb.InnerText -ne "Adopted") { + Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'" + $ok = $false + } + $subName = $subProps.SelectSingleNode("md:Name", $nsm) + if (-not $subName -or -not $subName.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType} missing Name" + $ok = $false + } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + if (-not $subExt -or -not $subExt.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject" + $ok = $false + } elseif ($subExt.InnerText -notmatch $guidPattern) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject" + $ok = $false + } + return $ok +} + if ($childObjNode) { $borrowedCount = 0 $borrowedOk = 0 $check9Ok = $true + $check10Ok = $true + $subItemCount = 0 foreach ($child in $childObjNode.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } @@ -577,11 +612,11 @@ if ($childObjNode) { $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) if (-not $objProps) { continue } + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) if ($obNode -and $obNode.InnerText -eq "Adopted") { $borrowedCount++ - # Check ExtendedConfigurationObject $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) if (-not $extObj -or -not $extObj.InnerText) { Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" @@ -594,6 +629,90 @@ if ($childObjNode) { } } + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + $objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if ($objChildObjects) { + $ctx = "${typeName}.${childName}" + foreach ($subItem in $objChildObjects.ChildNodes) { + if ($subItem.NodeType -ne 'Element') { continue } + $subType = $subItem.LocalName + + if ($subType -eq "Attribute") { + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) { + $check10Ok = $false + } + } + elseif ($subType -eq "TabularSection") { + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) { + $check10Ok = $false + } else { + # Check InternalInfo GeneratedTypes + $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) + $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } + if (-not $tsInfo) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" + $check10Ok = $false + } else { + $gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs) + $hasTSCat = $false; $hasTSRCat = $false + foreach ($gt in $gtNodes) { + $cat = $gt.GetAttribute("category") + if ($cat -eq "TabularSection") { $hasTSCat = $true } + if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true } + } + if (-not $hasTSCat -or -not $hasTSRCat) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)" + $check10Ok = $false + } + } + # Recurse into TS ChildObjects/Attribute + $tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs) + if ($tsChildObjs) { + foreach ($tsAttr in $tsChildObjs.ChildNodes) { + if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) { + $check10Ok = $false + } + } + } + } + } + elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") { + $subItemCount++ + if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) { + $evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($evName -and $evName.InnerText) { + if (-not $script:enumValuesIndex.ContainsKey($childName)) { + $script:enumValuesIndex[$childName] = @{} + } + $script:enumValuesIndex[$childName][$evName.InnerText] = $true + } + } else { + $check10Ok = $false + } + } + elseif ($subType -eq "Form") { + $formName = $subItem.InnerText + if ($formName) { + $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml" + if (-not (Test-Path $formMetaFile)) { + Report-Error "10. ${ctx}: Form.${formName} metadata file missing" + $check10Ok = $false + } + $script:formList += @{ + TypeName = $typeName; ObjName = $childName + FormName = $formName; DirName = $dirName + } + $subItemCount++ + } + } + } + } + if ($script:stopped) { break } } @@ -602,6 +721,208 @@ if ($childObjNode) { } elseif ($check9Ok) { Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" } + + if ($subItemCount -eq 0) { + Report-OK "10. Sub-items: none found" + } elseif ($check10Ok) { + Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: Borrowed form structure --- +$script:borrowedFormsWithTree = @() +$check11Ok = $true +$formCount = 0 + +foreach ($fi in $script:formList) { + $formCount++ + $formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName + $formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml" + $formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml" + $moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl" + $ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)" + + # Validate form metadata XML + if (Test-Path $formMetaFile) { + try { + $fmDoc = New-Object System.Xml.XmlDocument + $fmDoc.PreserveWhitespace = $false + $fmDoc.Load($formMetaFile) + $fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable) + $fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $fmEl = $null + foreach ($c in $fmDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $fmEl = $c; break } + } + if ($fmEl) { + $fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs) + if ($fmProps) { + $fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs) + $isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted" + if ($isBorrowed) { + $fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs) + if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) { + Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject" + $check11Ok = $false + } + } + $fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs) + if ($fmType -and $fmType.InnerText -ne "Managed") { + Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'" + $check11Ok = $false + } + } + } + } catch { + Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)" + } + } + + # Form.xml must exist + if (-not (Test-Path $formXmlFile)) { + Report-Error "11. ${ctx}: Ext/Form.xml missing" + $check11Ok = $false + continue + } + + # Module.bsl should exist + if (-not (Test-Path $moduleBslFile)) { + Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing" + } + + # Read Form.xml as raw text for BaseForm checks + $formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8) + + if ($formRawText -match ']+version=') { + Report-Warn "11. ${ctx}: missing version attribute" + } + # Extract BaseForm content for DataPath/TitleDataPath checks (only inside BaseForm, not in extension elements) + $bfMatch = [regex]::Match($formRawText, '(?s)]*>(.+)') + if ($bfMatch.Success) { + $bfContent = $bfMatch.Groups[1].Value + if ($bfContent -match '[^<]+') { + Report-Warn "11. ${ctx}: found in BaseForm (should be stripped for borrowed forms)" + } + if ($bfContent -match '[^<]+') { + Report-Warn "11. ${ctx}: found in BaseForm (should be stripped)" + } + } + + $script:borrowedFormsWithTree += @{ + Path = $formXmlFile; RawText = $formRawText; Context = $ctx + } + } +} + +if ($formCount -eq 0) { + Report-OK "11. Borrowed forms: none found" +} elseif ($check11Ok) { + $bfCount = $script:borrowedFormsWithTree.Count + Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 12: Form dependency references --- +$platformStyleItems = @{ + "TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true + "FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true + "FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true + "ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true + "TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true + "SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true + "NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true + "ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true +} +$check12Ok = $true +$depCheckCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $missingItems = @() + + # CommonPicture references + $cpRefs = @{} + foreach ($m in [regex]::Matches($raw, 'CommonPicture\.(\w+)')) { + $cpRefs[$m.Groups[1].Value] = $true + } + $cpIndex = $script:childObjectIndex["CommonPicture"] + foreach ($cpName in $cpRefs.Keys) { + $depCheckCount++ + if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) { + $missingItems += "CommonPicture.${cpName}" + } + } + + # StyleItem references + $siRefs = @{} + foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) { + $siRefs[$m.Groups[1].Value] = $true + } + $siIndex = $script:childObjectIndex["StyleItem"] + foreach ($siName in $siRefs.Keys) { + $depCheckCount++ + if ($platformStyleItems.ContainsKey($siName)) { continue } + if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) { + $missingItems += "StyleItem.${siName}" + } + } + + # Enum DesignTimeRef references + $enumRefs = @{} + foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) { + $eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)" + $enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value } + } + $eIndex = $script:childObjectIndex["Enum"] + foreach ($entry in $enumRefs.Values) { + $depCheckCount++ + if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) { + $missingItems += "Enum.$($entry.Enum)" + } elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) { + $missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)" + } + } + + foreach ($mi in $missingItems) { + Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" + $check12Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "12. Form dependencies: no borrowed forms with tree" +} elseif ($check12Ok) { + Report-OK "12. Form dependencies: $depCheckCount references checked" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 13: TypeLink with human-readable paths --- +$check13Ok = $true +$typeLinkCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $matches = [regex]::Matches($raw, '\s*Items\.[^<]*') + if ($matches.Count -gt 0) { + $typeLinkCount += $matches.Count + Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)" + $check13Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "13. TypeLink: no borrowed forms with tree" +} elseif ($check13Ok) { + Report-OK "13. TypeLink: clean" } # --- Final output --- diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py index 40d24db0..4b85a9b8 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.py +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-validate v1.1 — Validate 1C configuration extension XML structure (CFE) +# cfe-validate v1.2 — Validate 1C configuration extension XML structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" import sys, os, argparse, re @@ -392,7 +392,7 @@ def main(): else: check5_ok = True total_count = 0 - type_counts = {} + child_object_index = {} duplicates = {} type_first_index = {} last_type_order = -1 @@ -420,20 +420,20 @@ def main(): order_ok = False last_type_order = type_idx - if type_name not in type_counts: - type_counts[type_name] = {} - if obj_name_val in type_counts[type_name]: + if type_name not in child_object_index: + child_object_index[type_name] = {} + if obj_name_val in child_object_index[type_name]: dup_key = f'{type_name}.{obj_name_val}' if dup_key not in duplicates: r.error(f'5. Duplicate: {dup_key}') duplicates[dup_key] = True check5_ok = False else: - type_counts[type_name][obj_name_val] = True + child_object_index[type_name][obj_name_val] = True total_count += 1 - type_count = len(type_counts) + type_count = len(child_object_index) if check5_ok: order_info = ', order correct' if order_ok else '' r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}') @@ -529,11 +529,43 @@ def main(): r.finalize(out_file) sys.exit(1) - # --- Check 9: Borrowed objects validation --- + # --- Check 9: Borrowed objects + Check 10: Sub-items --- + MD = NS['md'] + XR = NS['xr'] + enum_values_index = {} + form_list = [] + + def validate_borrowed_sub_item(check_num, context, sub_type, sub_item): + """Validate a borrowed Attribute/EnumValue/TabularSection sub-item.""" + sub_props = sub_item.find(f'{{{MD}}}Properties') + if sub_props is None: + r.error(f'{check_num}. {context}: {sub_type} missing Properties') + return False + ok = True + sub_ob = sub_props.find(f'{{{MD}}}ObjectBelonging') + if sub_ob is None or (sub_ob.text or '') != 'Adopted': + r.error(f"{check_num}. {context}: {sub_type} ObjectBelonging must be 'Adopted'") + ok = False + sub_name = sub_props.find(f'{{{MD}}}Name') + if sub_name is None or not (sub_name.text or ''): + r.error(f'{check_num}. {context}: {sub_type} missing Name') + ok = False + sub_ext = sub_props.find(f'{{{MD}}}ExtendedConfigurationObject') + sub_name_val = (sub_name.text or '') if sub_name is not None else '?' + if sub_ext is None or not (sub_ext.text or ''): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} missing ExtendedConfigurationObject') + ok = False + elif not GUID_PATTERN.match(sub_ext.text): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} invalid ExtendedConfigurationObject') + ok = False + return ok + if child_obj_node is not None: borrowed_count = 0 borrowed_ok_count = 0 check9_ok = True + check10_ok = True + sub_item_count = 0 for child in child_obj_node: if not isinstance(child.tag, str): @@ -552,7 +584,6 @@ def main(): continue # Parse object XML - obj_doc = None try: obj_parser = etree.XMLParser(remove_blank_text=False) obj_doc = etree.parse(obj_file, obj_parser) @@ -571,16 +602,16 @@ def main(): if obj_el is None: continue - obj_props = obj_el.find(f'{{{NS["md"]}}}Properties') + obj_props = obj_el.find(f'{{{MD}}}Properties') if obj_props is None: continue - ob_node = obj_props.find(f'{{{NS["md"]}}}ObjectBelonging') + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- + ob_node = obj_props.find(f'{{{MD}}}ObjectBelonging') if ob_node is not None and (ob_node.text or '') == 'Adopted': borrowed_count += 1 - # Check ExtendedConfigurationObject - ext_obj = obj_props.find(f'{{{NS["md"]}}}ExtendedConfigurationObject') + ext_obj = obj_props.find(f'{{{MD}}}ExtendedConfigurationObject') if ext_obj is None or not (ext_obj.text or ''): r.error(f'9. Borrowed {type_name}.{child_name}: missing ExtendedConfigurationObject') check9_ok = False @@ -590,14 +621,256 @@ def main(): else: borrowed_ok_count += 1 + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') + if obj_child_objects is not None: + ctx = f'{type_name}.{child_name}' + for sub_item in obj_child_objects: + if not isinstance(sub_item.tag, str): + continue + sub_type = etree.QName(sub_item.tag).localname + + if sub_type == 'Attribute': + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'Attribute', sub_item): + check10_ok = False + + elif sub_type == 'TabularSection': + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'TabularSection', sub_item): + check10_ok = False + else: + # Check InternalInfo GeneratedTypes + ts_info = sub_item.find(f'{{{MD}}}InternalInfo') + ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' + if ts_info is None: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') + check10_ok = False + else: + gt_nodes = ts_info.findall(f'{{{XR}}}GeneratedType') + has_ts = any(gt.get('category') == 'TabularSection' for gt in gt_nodes) + has_tsr = any(gt.get('category') == 'TabularSectionRow' for gt in gt_nodes) + if not has_ts or not has_tsr: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing GeneratedType (need TabularSection + TabularSectionRow)') + check10_ok = False + # Recurse into TS ChildObjects/Attribute + ts_child_objs = sub_item.find(f'{{{MD}}}ChildObjects') + if ts_child_objs is not None: + for ts_attr in ts_child_objs: + if not isinstance(ts_attr.tag, str): + continue + if etree.QName(ts_attr.tag).localname != 'Attribute': + continue + sub_item_count += 1 + if not validate_borrowed_sub_item('10', f'{ctx}.ТЧ.{ts_label}', 'Attribute', ts_attr): + check10_ok = False + + elif sub_type == 'EnumValue' and type_name == 'Enum': + sub_item_count += 1 + if validate_borrowed_sub_item('10', ctx, 'EnumValue', sub_item): + ev_name = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + if ev_name is not None and (ev_name.text or ''): + if child_name not in enum_values_index: + enum_values_index[child_name] = {} + enum_values_index[child_name][ev_name.text] = True + else: + check10_ok = False + + elif sub_type == 'Form': + form_name = sub_item.text or '' + if form_name: + form_meta_file = os.path.join(config_dir, dir_name, child_name, 'Forms', form_name + '.xml') + if not os.path.exists(form_meta_file): + r.error(f'10. {ctx}: Form.{form_name} metadata file missing') + check10_ok = False + form_list.append({ + 'TypeName': type_name, 'ObjName': child_name, + 'FormName': form_name, 'DirName': dir_name, + }) + sub_item_count += 1 + if r.stopped: break if borrowed_count == 0: - pass # no borrowed objects + r.ok('9. Borrowed objects: none found') elif check9_ok: r.ok(f'9. Borrowed objects: {borrowed_ok_count}/{borrowed_count} validated') + if sub_item_count == 0: + r.ok('10. Sub-items: none found') + elif check10_ok: + r.ok(f'10. Sub-items: {sub_item_count} validated (Attributes, TabularSections, EnumValues, Forms)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 11: Borrowed form structure --- + borrowed_forms_with_tree = [] + check11_ok = True + form_count = 0 + + for fi in form_list: + form_count += 1 + form_base = os.path.join(config_dir, fi['DirName'], fi['ObjName'], 'Forms', fi['FormName']) + form_meta_file = os.path.join(os.path.dirname(form_base), fi['FormName'] + '.xml') + form_xml_file = os.path.join(form_base, 'Ext', 'Form.xml') + module_bsl_file = os.path.join(form_base, 'Ext', 'Form', 'Module.bsl') + ctx = f"{fi['TypeName']}.{fi['ObjName']}.Form.{fi['FormName']}" + + # Validate form metadata XML + if os.path.exists(form_meta_file): + try: + fm_doc = etree.parse(form_meta_file, etree.XMLParser(remove_blank_text=False)) + fm_root = fm_doc.getroot() + fm_el = None + for c in fm_root: + if isinstance(c.tag, str): + fm_el = c + break + if fm_el is not None: + fm_props = fm_el.find(f'{{{MD}}}Properties') + if fm_props is not None: + fm_ob = fm_props.find(f'{{{MD}}}ObjectBelonging') + is_borrowed = fm_ob is not None and (fm_ob.text or '') == 'Adopted' + if is_borrowed: + fm_ext = fm_props.find(f'{{{MD}}}ExtendedConfigurationObject') + if fm_ext is None or not (fm_ext.text or '') or not GUID_PATTERN.match(fm_ext.text or ''): + r.error(f'11. {ctx}: invalid/missing ExtendedConfigurationObject') + check11_ok = False + fm_type = fm_props.find(f'{{{MD}}}FormType') + if fm_type is not None and (fm_type.text or '') != 'Managed': + r.error(f"11. {ctx}: FormType must be 'Managed', got '{fm_type.text}'") + check11_ok = False + except etree.XMLSyntaxError as e: + r.warn(f'11. {ctx}: Cannot parse metadata: {e}') + + # Form.xml must exist + if not os.path.exists(form_xml_file): + r.error(f'11. {ctx}: Ext/Form.xml missing') + check11_ok = False + continue + + # Module.bsl should exist + if not os.path.exists(module_bsl_file): + r.warn(f'11. {ctx}: Ext/Form/Module.bsl missing') + + # Read Form.xml as raw text for BaseForm checks + with open(form_xml_file, 'r', encoding='utf-8-sig') as f: + form_raw_text = f.read() + + if ']+version=', form_raw_text): + r.warn(f'11. {ctx}: missing version attribute') + # Check DataPath/TitleDataPath only inside BaseForm (not in extension-added elements) + bf_match = re.search(r'(?s)]*>(.+)', form_raw_text) + if bf_match: + bf_content = bf_match.group(1) + if re.search(r'[^<]+', bf_content): + r.warn(f'11. {ctx}: found in BaseForm (should be stripped for borrowed forms)') + if re.search(r'[^<]+', bf_content): + r.warn(f'11. {ctx}: found in BaseForm (should be stripped)') + borrowed_forms_with_tree.append({ + 'Path': form_xml_file, 'RawText': form_raw_text, 'Context': ctx, + }) + + if form_count == 0: + r.ok('11. Borrowed forms: none found') + elif check11_ok: + bf_count = len(borrowed_forms_with_tree) + r.ok(f'11. Borrowed forms: {form_count} validated ({bf_count} with BaseForm)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 12: Form dependency references --- + PLATFORM_STYLE_ITEMS = { + 'TableHeaderBackColor', 'AccentColor', 'NormalTextFont', + 'FormBackColor', 'ToolTipBackColor', 'BorderColor', + 'FieldBackColor', 'FieldTextColor', 'ButtonBackColor', + 'ButtonTextColor', 'AlternateRowColor', 'SpecialTextColor', + 'TextFont', 'ImportantColor', 'FormTextColor', + 'SmallTextFont', 'ExtraLargeTextFont', 'LargeTextFont', + 'NormalTextColor', 'GroupHeaderBackColor', 'GroupHeaderFont', + 'ErrorColor', 'SuccessColor', 'WarningColor', + } + check12_ok = True + dep_check_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + missing_items = [] + + # CommonPicture references + cp_refs = {} + for m in re.finditer(r'CommonPicture\.(\w+)', raw): + cp_refs[m.group(1)] = True + cp_index = child_object_index.get('CommonPicture', {}) + for cp_name in cp_refs: + dep_check_count += 1 + if cp_name not in cp_index: + missing_items.append(f'CommonPicture.{cp_name}') + + # StyleItem references + si_refs = {} + for m in re.finditer(r'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)', raw): + si_refs[m.group(1)] = True + si_index = child_object_index.get('StyleItem', {}) + for si_name in si_refs: + dep_check_count += 1 + if si_name in PLATFORM_STYLE_ITEMS: + continue + if si_name not in si_index: + missing_items.append(f'StyleItem.{si_name}') + + # Enum DesignTimeRef references + enum_refs = {} + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', raw): + e_key = f'{m.group(1)}.{m.group(2)}' + enum_refs[e_key] = {'Enum': m.group(1), 'Value': m.group(2)} + e_index = child_object_index.get('Enum', {}) + for entry in enum_refs.values(): + dep_check_count += 1 + if entry['Enum'] not in e_index: + missing_items.append(f"Enum.{entry['Enum']}") + elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): + missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") + + for mi in missing_items: + r.warn(f'12. {ctx}: references {mi} not borrowed in extension') + check12_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('12. Form dependencies: no borrowed forms with tree') + elif check12_ok: + r.ok(f'12. Form dependencies: {dep_check_count} references checked') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 13: TypeLink with human-readable paths --- + check13_ok = True + type_link_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + matches = re.findall(r'\s*Items\.[^<]*', raw) + if matches: + type_link_count += len(matches) + r.warn(f'13. {ctx}: {len(matches)} TypeLink(s) with human-readable Items.* DataPath (should be stripped)') + check13_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('13. TypeLink: no borrowed forms with tree') + elif check13_ok: + r.ok('13. TypeLink: clean') + # --- Final output --- r.finalize(out_file) sys.exit(1 if r.errors > 0 else 0) From 85840214465192e3e79efdd9521d458abbae0815 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 21:00:20 +0300 Subject: [PATCH 7/8] fix(cfe-validate): remove false DataPath/TitleDataPath check from Check 11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configurator stores DataPath inside as original form snapshot — this is normal behavior, not an error. Removes false WARN on Configurator-exported extensions like JR2433. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/cfe-validate/SKILL.md | 2 +- .claude/skills/cfe-validate/scripts/cfe-validate.ps1 | 11 ----------- .claude/skills/cfe-validate/scripts/cfe-validate.py | 8 -------- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index c4e013eb..00d1e3b9 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -42,7 +42,7 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate | 8 | Каталоги объектов существуют | WARN | | 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN | | 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR | -| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm (DataPath, TitleDataPath) | ERROR/WARN | +| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm version | ERROR/WARN | | 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN | | 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN | diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index 0c665f84..62d8c498 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -801,17 +801,6 @@ foreach ($fi in $script:formList) { if ($formRawText -notmatch ']+version=') { Report-Warn "11. ${ctx}: missing version attribute" } - # Extract BaseForm content for DataPath/TitleDataPath checks (only inside BaseForm, not in extension elements) - $bfMatch = [regex]::Match($formRawText, '(?s)]*>(.+)') - if ($bfMatch.Success) { - $bfContent = $bfMatch.Groups[1].Value - if ($bfContent -match '[^<]+') { - Report-Warn "11. ${ctx}: found in BaseForm (should be stripped for borrowed forms)" - } - if ($bfContent -match '[^<]+') { - Report-Warn "11. ${ctx}: found in BaseForm (should be stripped)" - } - } $script:borrowedFormsWithTree += @{ Path = $formXmlFile; RawText = $formRawText; Context = $ctx diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py index 4b85a9b8..fad924d0 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.py +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -764,14 +764,6 @@ def main(): if ']+version=', form_raw_text): r.warn(f'11. {ctx}: missing version attribute') - # Check DataPath/TitleDataPath only inside BaseForm (not in extension-added elements) - bf_match = re.search(r'(?s)]*>(.+)', form_raw_text) - if bf_match: - bf_content = bf_match.group(1) - if re.search(r'[^<]+', bf_content): - r.warn(f'11. {ctx}: found in BaseForm (should be stripped for borrowed forms)') - if re.search(r'[^<]+', bf_content): - r.warn(f'11. {ctx}: found in BaseForm (should be stripped)') borrowed_forms_with_tree.append({ 'Path': form_xml_file, 'RawText': form_raw_text, 'Context': ctx, }) From 84d078bd05bb8a205c4da16dbe0902aa627a0fde Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 10 Mar 2026 21:40:16 +0300 Subject: [PATCH 8/8] fix(cfe-borrow,form-edit,cfe-patch-method): fix borrowed form structure to match Configurator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cfe-borrow: keep AutoCommandBar ChildItems (buttons) with CommandName=0 instead of stripping them — Configurator expects buttons to be present - form-edit: insert Events section after AutoCommandBar, not before — matches Configurator's element ordering - cfe-patch-method(py): fix \r\r\n double line endings by using newline="" in open() calls Co-Authored-By: Claude Opus 4.6 --- .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 4 ++-- .claude/skills/cfe-borrow/scripts/cfe-borrow.py | 4 ++-- .../scripts/cfe-patch-method.py | 6 +++--- .claude/skills/form-edit/scripts/form-edit.ps1 | 17 ++++++++++++----- .claude/skills/form-edit/scripts/form-edit.py | 10 +++++++++- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index a64ec15b..22a6cff2 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -500,12 +500,12 @@ function Borrow-Form { # Get OuterXml and strip redundant namespace redeclarations (they're on root ) $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' - # AutoCommandBar: strip ChildItems (buttons), replace CommandName→0, Autofill→false + # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false $autoCmdXml = "" if ($srcAutoCmd) { $autoCmdXml = $srcAutoCmd.OuterXml $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') - $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*.*?', '') + $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') $autoCmdXml = $autoCmdXml -replace 'true', 'false' } diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index dca7c817..4c082d2e 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -671,12 +671,12 @@ def main(): ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') - # AutoCommandBar: strip ChildItems (buttons), replace CommandName→0, Autofill→false + # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false auto_cmd_xml = "" if src_auto_cmd is not None: auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) - auto_cmd_xml = re.sub(r'\s*.*?', '', auto_cmd_xml, flags=re.DOTALL) + auto_cmd_xml = re.sub(r'[^<]*', '0', auto_cmd_xml) auto_cmd_xml = auto_cmd_xml.replace('true', 'false') # ChildItems: copy full tree, clean up base-config references diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py index 5901693e..490fb938 100644 --- a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py @@ -205,7 +205,7 @@ def main(): if os.path.isfile(bsl_file): # Append to existing file - with open(bsl_file, "r", encoding="utf-8-sig") as f: + with open(bsl_file, "r", encoding="utf-8-sig", newline="") as f: existing = f.read() separator = "\r\n" @@ -213,11 +213,11 @@ def main(): separator = "\r\n\r\n" new_content = existing + separator + bsl_text - with open(bsl_file, "w", encoding="utf-8-sig") as f: + with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f: f.write(new_content) print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл else: - with open(bsl_file, "w", encoding="utf-8-sig") as f: + with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f: f.write(bsl_text) print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index 12b4d053..e80afe6e 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1078,11 +1078,18 @@ if ($def.formEvents -and $def.formEvents.Count -gt 0) { $eventsSection = $xmlDoc.CreateElement("Events", $formNs) $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) if ($insertAfter) { - $refNode = $insertAfter - $ws = $xmlDoc.CreateWhitespace("`r`n`t") - # Insert before the AutoCommandBar (Events come before AutoCommandBar in 1C) - $root.InsertBefore($ws, $refNode) | Out-Null - $root.InsertBefore($eventsSection, $refNode) | Out-Null + # Insert after AutoCommandBar (Events come after AutoCommandBar in 1C) + $ws1 = $xmlDoc.CreateWhitespace("`r`n`t") + $ws2 = $xmlDoc.CreateWhitespace("`r`n`t") + if ($insertAfter.NextSibling) { + $root.InsertBefore($ws1, $insertAfter.NextSibling) | Out-Null + $root.InsertBefore($eventsSection, $ws1) | Out-Null + $root.InsertBefore($ws2, $eventsSection) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($eventsSection) | Out-Null + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n")) | Out-Null + } } else { $firstChild = $root.FirstChild if ($firstChild) { diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py index a9e3cd24..4aced255 100644 --- a/.claude/skills/form-edit/scripts/form-edit.py +++ b/.claude/skills/form-edit/scripts/form-edit.py @@ -1162,7 +1162,15 @@ form_events_list = defn.get("formEvents") or [] if form_events_list: events_section = root.find("f:Events", NS) if events_section is None: - events_section = etree.SubElement(root, f"{{{FORM_NS}}}Events") + events_section = etree.Element(f"{{{FORM_NS}}}Events") + # Insert after AutoCommandBar (Events come after AutoCommandBar in 1C) + acb_node = root.find("f:AutoCommandBar", NS) + if acb_node is not None: + acb_idx = list(root).index(acb_node) + acb_node.tail = (acb_node.tail or "") + "\r\n\t" + root.insert(acb_idx + 1, events_section) + else: + root.append(events_section) evt_child_indent = get_child_indent(events_section) if not evt_child_indent: