mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-11 16:34:57 +03:00
Merge branch 'dev'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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") # Создан файл модуля
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 элементов
|
||||
|
||||
|
||||
Reference in New Issue
Block a user