Merge branch 'dev'

This commit is contained in:
Nick Shirokov
2026-03-10 21:42:52 +03:00
9 changed files with 1060 additions and 117 deletions
+204 -20
View File
@@ -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,36 +476,209 @@ 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 ($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) {
$formProps += $fc.OuterXml
}
}
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
$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, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Replace Autofill true → false
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
}
# 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 in ChildItems too
# Replace all CommandName values with 0
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
} else {
$childItemsXml = "<ChildItems/>"
# Strip DataPath (references base form attributes not in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
# Strip TitleDataPath (e.g. Объект.Товары.RowsCount — invalid without base attributes)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '')
# Strip element-level Events (base form handlers not in extension)
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<Events>.*?</Events>', '')
# Collect CommonPicture references from ChildItems
$picRefs = [regex]::Matches($childItemsXml, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')
$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 <Picture> blocks referencing non-borrowed CommonPictures
$picBlockPattern = '(?s)\s*<Picture>\s*<xr:Ref>CommonPicture\.(\w+)</xr:Ref>.*?</Picture>'
$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*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '')
# Auto-borrow StyleItems referenced in ChildItems
# Pattern 1: <Font ref="style:XXX" kind="StyleItem"/>, <TitleFont ref="style:XXX" ... kind="StyleItem"/>
# Pattern 2: <BackColor>style:XXX</BackColor>, <TextColor>style:XXX</TextColor>, 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+)</\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 += @"
<EnumValue uuid="${newEvUuid}">
<InternalInfo/>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>$($evNameNode.InnerText)</Name>
<Comment/>
<ExtendedConfigurationObject>${evUuid}</ExtendedConfigurationObject>
</Properties>
</EnumValue>
"@
}
}
# 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 '<ChildObjects/>', "<ChildObjects>`r`n${evBlock}`r`n`t`t</ChildObjects>"
}
$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 <Form ...> opening tag from source text (preserves namespace declarations)
@@ -521,22 +694,31 @@ 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 + AutoCommandBar + ChildItems
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
}
$formXmlSb.Append("`t$childItemsXml") | 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<Attributes/>") | Out-Null
$formXmlSb.Append("`r`n") | Out-Null
# BaseForm: same visual elements, indented one more level
# BaseForm: same content, indented one more level
$formXmlSb.Append("`t<BaseForm version=`"${formVersion}`">") | 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"
for ($li = 0; $li -lt $acLines.Count; $li++) {
if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null }
@@ -544,12 +726,14 @@ function Borrow-Form {
$formXmlSb.Append("`r`n") | Out-Null
}
}
$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
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<Attributes/>") | Out-Null
+169 -17
View File
@@ -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
@@ -652,18 +652,26 @@ 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:
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:
src_child_items = fc
continue
if ln in ("ChildItems", "Events", "Attributes", "Commands", "Parameters"):
reached_visual = True
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+)?="[^"]*"')
# 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")
@@ -671,15 +679,151 @@ def main():
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# 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'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
else:
child_items_xml = "<ChildItems/>"
# Strip DataPath
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
# Strip TitleDataPath
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
child_items_xml = re.sub(r'\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '', child_items_xml, flags=re.DOTALL)
# Strip element-level Events
child_items_xml = re.sub(r'\s*<Events>.*?</Events>', '', child_items_xml, flags=re.DOTALL)
# Extract source form opening tag
# Auto-borrow referenced CommonPictures
pic_refs = re.findall(r'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', 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 <Picture> blocks referencing non-borrowed CommonPictures (reverse order)
pic_block_pattern = re.compile(r'\s*<Picture>\s*<xr:Ref>CommonPicture\.(\w+)</xr:Ref>.*?</Picture>', 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*<Picture>\s*<xr:Ref>StdPicture\.(?!Print\b)\w+</xr:Ref>.*?</Picture>', '', 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+)</\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<EnumValue uuid="{new_ev_uuid}">\n'
f'\t\t\t\t<InternalInfo/>\n'
f'\t\t\t\t<Properties>\n'
f'\t\t\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>\n'
f'\t\t\t\t\t<Name>{name_el.text.strip()}</Name>\n'
f'\t\t\t\t\t<Comment/>\n'
f'\t\t\t\t\t<ExtendedConfigurationObject>{ev_uuid}</ExtendedConfigurationObject>\n'
f'\t\t\t\t</Properties>\n'
f'\t\t\t</EnumValue>'
)
# 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("<ChildObjects/>", f"<ChildObjects>\n{ev_block}\n\t\t</ChildObjects>")
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 <Form ...> opening tag from source text
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
form_tag = f'<Form version="{form_version}">'
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
@@ -696,14 +840,22 @@ def main():
parts.append(form_tag)
parts.append("\r\n")
# 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")
parts.append(f"\t{child_items_xml}\r\n")
if child_items_xml:
parts.append(f"\t{child_items_xml}\r\n")
parts.append("\t<Attributes/>\r\n")
# BaseForm
# BaseForm: same content, indented one more level
parts.append(f'\t<BaseForm version="{form_version}">\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):
@@ -712,14 +864,14 @@ def main():
else:
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")
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<Attributes/>\r\n")
parts.append("\t</BaseForm>\r\n")
@@ -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") # Создан файл модуля
+5 -1
View File
@@ -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 version | ERROR/WARN |
| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN |
| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN |
Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации.
@@ -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,197 @@ 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 '<BaseForm') {
# Check BaseForm has version
if ($formRawText -notmatch '<BaseForm[^>]+version=') {
Report-Warn "11. ${ctx}: <BaseForm> missing version attribute"
}
$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, '<xr:Ref>CommonPicture\.(\w+)</xr:Ref>')) {
$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, '<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>')
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 ---
@@ -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,248 @@ 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 '<BaseForm' in form_raw_text:
if not re.search(r'<BaseForm[^>]+version=', form_raw_text):
r.warn(f'11. {ctx}: <BaseForm> missing version attribute')
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'<xr:Ref>CommonPicture\.(\w+)</xr:Ref>', 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'<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>', 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)
+12 -5
View File
@@ -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) {
@@ -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:
+61 -48
View File
@@ -356,71 +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
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" ... version="2.17">
<!-- ═══ ЧАСТЬ 1: Результирующая форма (база + изменения расширения) ═══ -->
<AutoTitle>false</AutoTitle>
<AutoTime>CurrentOrLast</AutoTime>
<!-- ... другие свойства формы ... -->
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<ChildItems>
<!-- Базовые кнопки: CommandName заменён на 0 -->
<Button name="ФормаОбработкаЗагрузитьИзФайла" id="51">
<CommandName>0</CommandName>
...
</Button>
<!-- Кнопки, добавленные расширением: CommandName указывает на команду -->
<Button name="ФормаНоваяКоманда" id="159">
<CommandName>Form.Command.НоваяКоманда</CommandName>
...
</Button>
</ChildItems>
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<!-- Все визуальные элементы: базовые + добавленные расширением -->
</ChildItems>
<Attributes/> <!-- пустой, ИЛИ только реквизиты расширения (id ≥ 1000000) -->
<!-- Events — только обработчики расширения с callType (если есть) -->
<Events>
<Event name="OnCreateAtServer" callType="After">Расш1_ПриСозданииПосле</Event>
</Events>
<!-- Commands — только команды расширения (id ≥ 1000000, если есть) -->
<Commands>
<Command name="НоваяКоманда" id="1000000">
<Action callType="Override">Расш1_НоваяКомандаВместо</Action>
</Command>
</Commands>
<Attributes/>
<!-- Events, Commands — только расширения (если есть) -->
<!-- ═══ ЧАСТЬ 2: Исходная форма из базовой конфигурации ═══ -->
<BaseForm version="2.17">
<AutoTitle>false</AutoTitle>
<AutoTime>CurrentOrLast</AutoTime>
<!-- те же свойства -->
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<ChildItems>
<!-- Только базовые кнопки, все CommandName = 0 -->
<Button name="ФормаОбработкаЗагрузитьИзФайла" id="51">
<CommandName>0</CommandName>
...
</Button>
</ChildItems>
<Autofill>false</Autofill>
</AutoCommandBar>
<ChildItems>
<!-- Только визуальные элементы базовой конфигурации -->
</ChildItems>
<Attributes/> <!-- всегда пустой -->
<!-- НЕТ Events, Commands, Parameters -->
<Attributes/>
</BaseForm>
</Form>
```
**Ключевые правила:**
##### Вариант B — Полная форма (заимствование процедуры модуля через `ИзменениеИКонтроль`)
1. **Часть 1** (до `<BaseForm>`) — **результирующая форма**. Содержит визуальные элементы (AutoCommandBar + ChildItems) из базовой конфигурации плюс элементы расширения. Атрибуты базовой конфигурации (DynamicList, QueryText и др.) **не включаются** — только реквизиты расширения (id ≥ 1000000) или пустой `<Attributes/>`. Events и Commands — только добавленные расширением (с `callType`).
Когда в расширении заимствуется процедура из модуля формы, Конфигуратор выгружает **полное дерево ChildItems** с применением правил очистки.
2. **Часть 2** (`<BaseForm>`) — **визуальный снимок исходной формы**. Содержит только AutoCommandBar + ChildItems + пустой `<Attributes/>`. НЕ содержит Events, Commands, Parameters. Все `<CommandName>` в кнопках заменены на `0`. Платформа использует BaseForm для контроля совместимости при обновлении конфигурации.
```xml
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" ... version="2.17">
<AutoTitle>false</AutoTitle>
<!-- свойства формы -->
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>false</Autofill>
<!-- Без ChildItems (кнопки удалены) -->
</AutoCommandBar>
<ChildItems>
<!-- Полное дерево визуальных элементов -->
</ChildItems>
<Attributes/>
<!-- Events, Commands — только расширения -->
3. **Правило `<CommandName>0</CommandName>`**: во всех кнопках базовой формы (как в Part 1, так и в BaseForm) значение `<CommandName>` заменяется на `0`. Ссылки на команды конфигурации не сохраняются. Только кнопки, добавленные расширением, сохраняют ссылку на команду (напр. `Form.Command.XXX`).
<BaseForm version="2.17">
<!-- Идентичная копия (свойства + AutoCommandBar + ChildItems + Attributes) -->
</BaseForm>
</Form>
```
4. Элемент `<BaseForm>` всегда идёт **последним** в `<Form>` и имеет атрибут `version`.
**Ключевые правила (для обоих вариантов):**
1. **Свойства формы** — элементы между `<Form>` и `<AutoCommandBar>` (напр. `AutoTitle`, `AutoTime`, `UsePostingMode`, `RepostOnWrite`, `WindowOpeningMode`, `Customizable`, `CommandBarLocation`) копируются из исходной формы в обе секции.
2. **AutoCommandBar** — присутствует всегда с `id="-1"`, но без `<ChildItems>` (кнопки удаляются). `<Autofill>` = `false`.
3. **Attributes** — пустой `<Attributes/>` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1.
4. **BaseForm** — последний элемент в `<Form>`, атрибут `version`. В BaseForm **нет** Events, Commands, Parameters.
5. **DataPath: удаление** (вариант B) — все `<DataPath>` из базовых элементов удаляются в обеих секциях. DataPath ссылается на реквизиты, не включённые в расширение.
6. **TitleDataPath: удаление** (вариант B) — все `<TitleDataPath>` удаляются (напр. `Объект.Товары.RowsCount` — путь недействителен без базовых атрибутов).
7. **TypeLink: удаление** (вариант B) — блоки `<TypeLink>` с `<xr:DataPath>Items.*</xr:DataPath>` удаляются (человекочитаемые пути, которые нельзя преобразовать в UUID-формат Конфигуратора).
8. **Events элементов: удаление** (вариант B) — все `<Events>` внутри визуальных элементов удаляются в обеих секциях. Обработчики расширения добавляются через `elementEvents` в Part 1 с `callType`.
9. **Picture stripping** (вариант B) — блоки `<Picture>` с `<xr:Ref>CommonPicture.XXX</xr:Ref>` удаляются, если `CommonPicture.XXX` **не заимствован** в расширение. Сам элемент PictureDecoration остаётся, только `<Picture>` убирается. `StdPicture.Print` сохраняется, остальные StdPicture удаляются.
10. **Авто-заимствование CommonPictures** — при заимствовании формы автоматически заимствуются все CommonPictures, на которые ссылаются элементы формы.
11. **Авто-заимствование StyleItems** — элементы формы ссылаются на StyleItems через `<Font ref="style:XXX" kind="StyleItem"/>` и `<BackColor>style:XXX</BackColor>`. Все такие StyleItems должны быть заимствованы. Стандартные стили (NormalTextFont, AccentColor, FormBackColor и др.) не имеют файлов и автоматически пропускаются.
12. **Авто-заимствование Enums + EnumValues**`<ChoiceParameters>` могут содержать `<Value xsi:type="xr:DesignTimeRef">Enum.XXX.EnumValue.YYY</Value>`. Перечисление `Enum.XXX` заимствуется вместе с конкретными `EnumValue` (borrowed с `ExtendedConfigurationObject` указывающим на UUID оригинального значения).
#### 5.4.3. Нумерация ID элементов