mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-13 01:14:56 +03:00
Auto-build: copilot (python) from 7fa279c
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
# form-info v1.3 — Analyze 1C managed form structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias('Path')]
|
||||
[string]$FormPath,
|
||||
[int]$Limit = 150,
|
||||
[int]$Offset = 0,
|
||||
[string]$Expand
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve FormPath ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($FormPath)) {
|
||||
$FormPath = Join-Path (Get-Location).Path $FormPath
|
||||
}
|
||||
# A: Directory → Ext/Form.xml
|
||||
if (Test-Path $FormPath -PathType Container) {
|
||||
$FormPath = Join-Path (Join-Path $FormPath "Ext") "Form.xml"
|
||||
}
|
||||
# B1: Missing Ext/ (Forms/Форма/Form.xml → Forms/Форма/Ext/Form.xml)
|
||||
if (-not (Test-Path $FormPath)) {
|
||||
$fn = [System.IO.Path]::GetFileName($FormPath)
|
||||
if ($fn -eq "Form.xml") {
|
||||
$c = Join-Path (Join-Path (Split-Path $FormPath) "Ext") $fn
|
||||
if (Test-Path $c) { $FormPath = $c }
|
||||
}
|
||||
}
|
||||
# B2: Descriptor (Forms/Форма.xml → Forms/Форма/Ext/Form.xml)
|
||||
if (-not (Test-Path $FormPath) -and $FormPath.EndsWith(".xml")) {
|
||||
$stem = [System.IO.Path]::GetFileNameWithoutExtension($FormPath)
|
||||
$dir = Split-Path $FormPath
|
||||
$c = Join-Path (Join-Path (Join-Path $dir $stem) "Ext") "Form.xml"
|
||||
if (Test-Path $c) { $FormPath = $c }
|
||||
}
|
||||
|
||||
if (-not (Test-Path $FormPath)) {
|
||||
Write-Error "File not found: $FormPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Load XML ---
|
||||
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $false
|
||||
$xmlDoc.Load((Resolve-Path $FormPath).Path)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("d", "http://v8.1c.ru/8.3/xcf/logform")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config")
|
||||
$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings")
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
# --- Detect extension (BaseForm) ---
|
||||
$baseFormNode = $root.SelectSingleNode("d:BaseForm", $ns)
|
||||
$isExtension = ($baseFormNode -ne $null)
|
||||
|
||||
# --- Helper: extract multilang text ---
|
||||
|
||||
function Get-MLText($node) {
|
||||
if (-not $node) { return "" }
|
||||
$content = $node.SelectSingleNode("v8:item/v8:content", $ns)
|
||||
if ($content) { return $content.InnerText }
|
||||
$text = $node.InnerText.Trim()
|
||||
if ($text) { return $text }
|
||||
return ""
|
||||
}
|
||||
|
||||
# --- Helper: format type compactly ---
|
||||
|
||||
function Format-Type($typeNode) {
|
||||
if (-not $typeNode -or -not $typeNode.HasChildNodes) { return "" }
|
||||
|
||||
$typeSet = $typeNode.SelectSingleNode("v8:TypeSet", $ns)
|
||||
if ($typeSet) {
|
||||
$val = $typeSet.InnerText
|
||||
# Strip cfg:/d5p1: prefix for DefinedType, keep as-is
|
||||
$val = $val -replace '^(cfg|d\d+p\d+):', ''
|
||||
return $val
|
||||
}
|
||||
|
||||
$types = $typeNode.SelectNodes("v8:Type", $ns)
|
||||
if ($types.Count -eq 0) { return "" }
|
||||
|
||||
$parts = @()
|
||||
foreach ($t in $types) {
|
||||
$raw = $t.InnerText
|
||||
switch -Wildcard ($raw) {
|
||||
"xs:string" {
|
||||
$sq = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns)
|
||||
$len = if ($sq) { [int]$sq.InnerText } else { 0 }
|
||||
if ($len -gt 0) { $parts += "string($len)" } else { $parts += "string" }
|
||||
}
|
||||
"xs:decimal" {
|
||||
$nq = $typeNode.SelectSingleNode("v8:NumberQualifiers", $ns)
|
||||
if ($nq) {
|
||||
$d = $nq.SelectSingleNode("v8:Digits", $ns)
|
||||
$f = $nq.SelectSingleNode("v8:FractionDigits", $ns)
|
||||
$digits = if ($d) { $d.InnerText } else { "0" }
|
||||
$frac = if ($f) { $f.InnerText } else { "0" }
|
||||
$parts += "decimal($digits,$frac)"
|
||||
} else {
|
||||
$parts += "decimal"
|
||||
}
|
||||
}
|
||||
"xs:boolean" { $parts += "boolean" }
|
||||
"xs:dateTime" {
|
||||
$dq = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns)
|
||||
if ($dq) {
|
||||
switch ($dq.InnerText) {
|
||||
"Date" { $parts += "date" }
|
||||
"Time" { $parts += "time" }
|
||||
default { $parts += "dateTime" }
|
||||
}
|
||||
} else {
|
||||
$parts += "dateTime"
|
||||
}
|
||||
}
|
||||
"xs:binary" { $parts += "binary" }
|
||||
{ $_ -like "cfg:*" -or $_ -match '^d\d+p\d+:' } { $parts += ($raw -replace '^(cfg|d\d+p\d+):', '') }
|
||||
"v8:ValueTable" { $parts += "ValueTable" }
|
||||
"v8:ValueTree" { $parts += "ValueTree" }
|
||||
"v8:ValueListType" { $parts += "ValueList" }
|
||||
"v8:TypeDescription" { $parts += "TypeDescription" }
|
||||
"v8:Universal" { $parts += "Universal" }
|
||||
"v8:FixedArray" { $parts += "FixedArray" }
|
||||
"v8:FixedStructure" { $parts += "FixedStructure" }
|
||||
"v8ui:FormattedString" { $parts += "FormattedString" }
|
||||
"v8ui:Picture" { $parts += "Picture" }
|
||||
"v8ui:Color" { $parts += "Color" }
|
||||
"v8ui:Font" { $parts += "Font" }
|
||||
"dcsset:*" { $parts += $raw.Replace("dcsset:", "DCS.") }
|
||||
"dcssch:*" { $parts += $raw.Replace("dcssch:", "DCS.") }
|
||||
"dcscor:*" { $parts += $raw.Replace("dcscor:", "DCS.") }
|
||||
default { $parts += $raw }
|
||||
}
|
||||
}
|
||||
|
||||
return ($parts -join " | ")
|
||||
}
|
||||
|
||||
# --- Helper: check if title differs from name ---
|
||||
|
||||
function Test-TitleDiffers($node, [string]$name) {
|
||||
$titleNode = $node.SelectSingleNode("d:Title", $ns)
|
||||
if (-not $titleNode) { return $null }
|
||||
$titleText = Get-MLText $titleNode
|
||||
if (-not $titleText) { return $null }
|
||||
# Normalize: remove spaces, lowercase
|
||||
$normTitle = ($titleText -replace '\s', '').ToLower()
|
||||
$normName = $name.ToLower()
|
||||
if ($normTitle -eq $normName) { return $null }
|
||||
return $titleText
|
||||
}
|
||||
|
||||
# --- Helper: get events as compact string ---
|
||||
|
||||
function Get-EventsStr($node) {
|
||||
$eventsNode = $node.SelectSingleNode("d:Events", $ns)
|
||||
if (-not $eventsNode) { return "" }
|
||||
$evts = @()
|
||||
foreach ($e in $eventsNode.SelectNodes("d:Event", $ns)) {
|
||||
$eName = $e.GetAttribute("name")
|
||||
$ct = $e.GetAttribute("callType")
|
||||
if ($ct) { $evts += "$eName[$ct]" }
|
||||
else { $evts += $eName }
|
||||
}
|
||||
if ($evts.Count -eq 0) { return "" }
|
||||
return " {$($evts -join ', ')}"
|
||||
}
|
||||
|
||||
# --- Helper: get flags ---
|
||||
|
||||
function Get-Flags($node) {
|
||||
$flags = @()
|
||||
$vis = $node.SelectSingleNode("d:Visible", $ns)
|
||||
if ($vis -and $vis.InnerText -eq "false") { $flags += "visible:false" }
|
||||
$en = $node.SelectSingleNode("d:Enabled", $ns)
|
||||
if ($en -and $en.InnerText -eq "false") { $flags += "enabled:false" }
|
||||
$ro = $node.SelectSingleNode("d:ReadOnly", $ns)
|
||||
if ($ro -and $ro.InnerText -eq "true") { $flags += "ro" }
|
||||
if ($flags.Count -eq 0) { return "" }
|
||||
return " [$($flags -join ',')]"
|
||||
}
|
||||
|
||||
# --- Element type abbreviations ---
|
||||
|
||||
$skipElements = @{
|
||||
"ExtendedTooltip" = $true
|
||||
"ContextMenu" = $true
|
||||
"AutoCommandBar" = $true
|
||||
"SearchStringAddition" = $true
|
||||
"ViewStatusAddition" = $true
|
||||
"SearchControlAddition" = $true
|
||||
"ColumnGroup" = $true
|
||||
}
|
||||
|
||||
function Get-ElementTag($node) {
|
||||
$localName = $node.LocalName
|
||||
switch ($localName) {
|
||||
"UsualGroup" {
|
||||
$groupNode = $node.SelectSingleNode("d:Group", $ns)
|
||||
$orient = ""
|
||||
if ($groupNode) {
|
||||
switch ($groupNode.InnerText) {
|
||||
"Vertical" { $orient = ":V" }
|
||||
"Horizontal" { $orient = ":H" }
|
||||
"AlwaysHorizontal" { $orient = ":AH" }
|
||||
"AlwaysVertical" { $orient = ":AV" }
|
||||
}
|
||||
}
|
||||
$beh = $node.SelectSingleNode("d:Behavior", $ns)
|
||||
$collapse = ""
|
||||
if ($beh -and $beh.InnerText -eq "Collapsible") { $collapse = ",collapse" }
|
||||
return "[Group$orient$collapse]"
|
||||
}
|
||||
"InputField" { return "[Input]" }
|
||||
"CheckBoxField" { return "[Check]" }
|
||||
"LabelDecoration" { return "[Label]" }
|
||||
"LabelField" { return "[LabelField]" }
|
||||
"PictureDecoration" { return "[Picture]" }
|
||||
"PictureField" { return "[PicField]" }
|
||||
"CalendarField" { return "[Calendar]" }
|
||||
"Table" { return "[Table]" }
|
||||
"Button" { return "[Button]" }
|
||||
"CommandBar" { return "[CmdBar]" }
|
||||
"Pages" { return "[Pages]" }
|
||||
"Page" { return "[Page]" }
|
||||
"Popup" { return "[Popup]" }
|
||||
"ButtonGroup" { return "[BtnGroup]" }
|
||||
default { return "[$localName]" }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Count significant children (for Page summary) ---
|
||||
|
||||
function Count-SignificantChildren($childItemsNode) {
|
||||
if (-not $childItemsNode) { return 0 }
|
||||
$count = 0
|
||||
foreach ($child in $childItemsNode.ChildNodes) {
|
||||
if ($child.NodeType -ne "Element") { continue }
|
||||
if ($skipElements.ContainsKey($child.LocalName)) { continue }
|
||||
$count++
|
||||
}
|
||||
return $count
|
||||
}
|
||||
|
||||
# --- Build element tree recursively ---
|
||||
|
||||
$treeLines = [System.Collections.Generic.List[string]]::new()
|
||||
$script:hasCollapsed = $false
|
||||
|
||||
function Build-Tree($childItemsNode, [string]$prefix, [bool]$isLast) {
|
||||
if (-not $childItemsNode) { return }
|
||||
|
||||
# Collect significant children
|
||||
$children = @()
|
||||
foreach ($child in $childItemsNode.ChildNodes) {
|
||||
if ($child.NodeType -ne "Element") { continue }
|
||||
if ($skipElements.ContainsKey($child.LocalName)) { continue }
|
||||
$children += $child
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $children.Count; $i++) {
|
||||
$child = $children[$i]
|
||||
$last = ($i -eq $children.Count - 1)
|
||||
$connector = if ($last) { [char]0x2514 + [string][char]0x2500 } else { [char]0x251C + [string][char]0x2500 }
|
||||
$continuation = if ($last) { " " } else { [string][char]0x2502 + " " }
|
||||
|
||||
$tag = Get-ElementTag $child
|
||||
$name = $child.GetAttribute("name")
|
||||
$flags = Get-Flags $child
|
||||
$events = Get-EventsStr $child
|
||||
|
||||
# DataPath or CommandName
|
||||
$binding = ""
|
||||
$dp = $child.SelectSingleNode("d:DataPath", $ns)
|
||||
if ($dp) {
|
||||
$binding = " -> $($dp.InnerText)"
|
||||
} else {
|
||||
$cn = $child.SelectSingleNode("d:CommandName", $ns)
|
||||
if ($cn) {
|
||||
$cnVal = $cn.InnerText
|
||||
if ($cnVal -match '^Form\.StandardCommand\.(.+)$') {
|
||||
$binding = " -> $($Matches[1]) [std]"
|
||||
} elseif ($cnVal -match '^Form\.Command\.(.+)$') {
|
||||
$binding = " -> $($Matches[1]) [cmd]"
|
||||
} else {
|
||||
$binding = " -> $cnVal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Title differs?
|
||||
$titleStr = ""
|
||||
$diffTitle = Test-TitleDiffers $child $name
|
||||
if ($diffTitle) { $titleStr = " [title:$diffTitle]" }
|
||||
|
||||
$line = "$prefix$connector $tag $name$binding$flags$titleStr$events"
|
||||
$treeLines.Add($line)
|
||||
|
||||
# Recurse into containers (but not Page — show summary unless expanded)
|
||||
$localName = $child.LocalName
|
||||
if ($localName -eq "Page") {
|
||||
$ci = $child.SelectSingleNode("d:ChildItems", $ns)
|
||||
$pageName = $child.GetAttribute("name")
|
||||
$pageTitle = Test-TitleDiffers $child $pageName
|
||||
$shouldExpand = ($Expand -eq "*") -or ($Expand -eq $pageName) -or ($pageTitle -and $Expand -eq $pageTitle)
|
||||
if ($shouldExpand -and $ci) {
|
||||
Build-Tree $ci "$prefix$continuation" $last
|
||||
} else {
|
||||
$cnt = Count-SignificantChildren $ci
|
||||
$idx = $treeLines.Count - 1
|
||||
$treeLines[$idx] = $treeLines[$idx] + " ($cnt items)"
|
||||
$script:hasCollapsed = $true
|
||||
}
|
||||
} elseif ($localName -in @("UsualGroup", "Pages", "Table", "CommandBar", "ButtonGroup", "Popup")) {
|
||||
$ci = $child.SelectSingleNode("d:ChildItems", $ns)
|
||||
if ($ci) {
|
||||
Build-Tree $ci "$prefix$continuation" $last
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Determine form name and object from path ---
|
||||
|
||||
$resolvedPath = (Resolve-Path $FormPath).Path
|
||||
$parts = $resolvedPath -split '[/\\]'
|
||||
|
||||
$formName = ""
|
||||
$objectContext = ""
|
||||
|
||||
# Look for /Forms/<FormName>/Ext/Form.xml pattern
|
||||
$formsIdx = -1
|
||||
for ($i = $parts.Count - 1; $i -ge 0; $i--) {
|
||||
if ($parts[$i] -eq "Forms") { $formsIdx = $i; break }
|
||||
}
|
||||
|
||||
if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
|
||||
$formName = $parts[$formsIdx + 1]
|
||||
# Object is 2 levels up: .../<ObjectType>/<ObjectName>/Forms/...
|
||||
if ($formsIdx -ge 2) {
|
||||
$objType = $parts[$formsIdx - 2]
|
||||
$objName = $parts[$formsIdx - 1]
|
||||
$objectContext = "$objType.$objName"
|
||||
}
|
||||
} else {
|
||||
# CommonForms pattern: .../<ObjectType>/<FormName>/Ext/Form.xml
|
||||
$extIdx = -1
|
||||
for ($i = $parts.Count - 1; $i -ge 0; $i--) {
|
||||
if ($parts[$i] -eq "Ext") { $extIdx = $i; break }
|
||||
}
|
||||
if ($extIdx -ge 2) {
|
||||
$formName = $parts[$extIdx - 1]
|
||||
$objType = $parts[$extIdx - 2]
|
||||
$objectContext = $objType
|
||||
} else {
|
||||
$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath)
|
||||
}
|
||||
}
|
||||
|
||||
# --- Collect output ---
|
||||
|
||||
$lines = @()
|
||||
|
||||
# Header — include Title if present
|
||||
$titleNode = $root.SelectSingleNode("d:Title", $ns)
|
||||
$formTitle = $null
|
||||
if ($titleNode) {
|
||||
$formTitle = Get-MLText $titleNode
|
||||
if (-not $formTitle) { $formTitle = $titleNode.InnerText }
|
||||
}
|
||||
$extMarker = if ($isExtension) { " [EXTENSION]" } else { "" }
|
||||
$header = "=== Form: $formName$extMarker"
|
||||
if ($formTitle) { $header += " — `"$formTitle`"" }
|
||||
if ($objectContext) { $header += " ($objectContext)" }
|
||||
$header += " ==="
|
||||
$lines += $header
|
||||
|
||||
# --- Form properties (Title excluded — shown in header) ---
|
||||
|
||||
$propNames = @(
|
||||
"Width", "Height", "Group",
|
||||
"WindowOpeningMode", "EnterKeyBehavior", "AutoTitle", "AutoURL",
|
||||
"AutoFillCheck", "Customizable", "CommandBarLocation",
|
||||
"SaveDataInSettings", "AutoSaveDataInSettings",
|
||||
"AutoTime", "UsePostingMode", "RepostOnWrite",
|
||||
"UseForFoldersAndItems",
|
||||
"ReportResult", "DetailsData", "ReportFormType",
|
||||
"VerticalScroll", "ScalingMode"
|
||||
)
|
||||
|
||||
$props = @()
|
||||
foreach ($pn in $propNames) {
|
||||
$pNode = $root.SelectSingleNode("d:$pn", $ns)
|
||||
if ($pNode) {
|
||||
$val = Get-MLText $pNode
|
||||
if (-not $val) { $val = $pNode.InnerText }
|
||||
$props += "$pn=$val"
|
||||
}
|
||||
}
|
||||
|
||||
if ($props.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "Properties: $($props -join ', ')"
|
||||
}
|
||||
|
||||
# --- Excluded commands ---
|
||||
|
||||
$excludedCmds = @()
|
||||
foreach ($ec in $root.SelectNodes("d:CommandSet/d:ExcludedCommand", $ns)) {
|
||||
$excludedCmds += $ec.InnerText
|
||||
}
|
||||
|
||||
# --- Form events ---
|
||||
|
||||
$formEvents = $root.SelectSingleNode("d:Events", $ns)
|
||||
if ($formEvents -and $formEvents.HasChildNodes) {
|
||||
$lines += ""
|
||||
$lines += "Events:"
|
||||
foreach ($e in $formEvents.SelectNodes("d:Event", $ns)) {
|
||||
$eName = $e.GetAttribute("name")
|
||||
$eHandler = $e.InnerText
|
||||
$ct = $e.GetAttribute("callType")
|
||||
$ctStr = if ($ct) { "[$ct]" } else { "" }
|
||||
$lines += " $eName${ctStr} -> $eHandler"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Main AutoCommandBar (form's id=-1 panel) ---
|
||||
|
||||
function Format-MainAcb($acbNode) {
|
||||
if (-not $acbNode) { return @() }
|
||||
$result = @()
|
||||
$autofillNode = $acbNode.SelectSingleNode("d:Autofill", $ns)
|
||||
$autofill = $true
|
||||
if ($autofillNode -and $autofillNode.InnerText -eq "false") { $autofill = $false }
|
||||
$halignNode = $acbNode.SelectSingleNode("d:HorizontalAlign", $ns)
|
||||
$flags = @()
|
||||
$flags += if ($autofill) { "autofill" } else { "no-autofill" }
|
||||
if ($halignNode) { $flags += "align=$($halignNode.InnerText)" }
|
||||
$header = "AutoCommandBar [$($flags -join ', ')]"
|
||||
$childItemsNode = $acbNode.SelectSingleNode("d:ChildItems", $ns)
|
||||
$buttons = @()
|
||||
if ($childItemsNode) {
|
||||
foreach ($btn in $childItemsNode.ChildNodes) {
|
||||
if ($btn.NodeType -ne "Element") { continue }
|
||||
if ($skipElements.ContainsKey($btn.LocalName)) { continue }
|
||||
$bName = $btn.GetAttribute("name")
|
||||
$cmdNode = $btn.SelectSingleNode("d:CommandName", $ns)
|
||||
$cmdRef = if ($cmdNode) { $cmdNode.InnerText } else { "" }
|
||||
$locNode = $btn.SelectSingleNode("d:LocationInCommandBar", $ns)
|
||||
$locStr = if ($locNode) { " [$($locNode.InnerText)]" } else { "" }
|
||||
$tag = Get-ElementTag $btn
|
||||
if ($cmdRef) {
|
||||
$buttons += " $tag $bName -> $cmdRef$locStr"
|
||||
} else {
|
||||
$buttons += " $tag $bName$locStr"
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($buttons.Count -eq 0 -and $autofill -and -not $halignNode) {
|
||||
# Default empty panel — terse one-liner
|
||||
return @("AutoCommandBar [autofill]")
|
||||
}
|
||||
$result += $header
|
||||
$result += $buttons
|
||||
return $result
|
||||
}
|
||||
|
||||
# Determine position from CommandBarLocation form property
|
||||
$cbLocNode = $root.SelectSingleNode("d:CommandBarLocation", $ns)
|
||||
$cbLoc = if ($cbLocNode) { $cbLocNode.InnerText } else { "Auto" }
|
||||
$mainAcbNode = $root.SelectSingleNode("d:AutoCommandBar", $ns)
|
||||
$acbLines = @()
|
||||
if ($cbLoc -ne "None" -and $mainAcbNode) {
|
||||
$acbLines = Format-MainAcb $mainAcbNode
|
||||
}
|
||||
|
||||
# AutoCommandBar above Elements (Auto/Top)
|
||||
if ($acbLines.Count -gt 0 -and ($cbLoc -eq "Auto" -or $cbLoc -eq "Top")) {
|
||||
$lines += ""
|
||||
$lines += $acbLines
|
||||
}
|
||||
|
||||
# --- Element tree ---
|
||||
|
||||
$childItems = $root.SelectSingleNode("d:ChildItems", $ns)
|
||||
if ($childItems) {
|
||||
$lines += ""
|
||||
$lines += "Elements:"
|
||||
Build-Tree $childItems " " $false
|
||||
$lines += $treeLines.ToArray()
|
||||
}
|
||||
|
||||
# AutoCommandBar below Elements (Bottom)
|
||||
if ($acbLines.Count -gt 0 -and $cbLoc -eq "Bottom") {
|
||||
$lines += ""
|
||||
$lines += $acbLines
|
||||
}
|
||||
|
||||
# --- Attributes ---
|
||||
|
||||
$attrsNode = $root.SelectSingleNode("d:Attributes", $ns)
|
||||
if ($attrsNode) {
|
||||
$attrLines = @()
|
||||
foreach ($attr in $attrsNode.SelectNodes("d:Attribute", $ns)) {
|
||||
$aName = $attr.GetAttribute("name")
|
||||
$typeNode = $attr.SelectSingleNode("d:Type", $ns)
|
||||
$typeStr = Format-Type $typeNode
|
||||
|
||||
$mainAttr = $attr.SelectSingleNode("d:MainAttribute", $ns)
|
||||
$isMain = ($mainAttr -and $mainAttr.InnerText -eq "true")
|
||||
|
||||
$prefix = if ($isMain) { "*" } else { " " }
|
||||
$mainSuffix = if ($isMain) { " (main)" } else { "" }
|
||||
|
||||
# DynamicList: show MainTable
|
||||
$settings = $attr.SelectSingleNode("d:Settings", $ns)
|
||||
$dynTable = ""
|
||||
if ($settings -and $typeStr -eq "DynamicList") {
|
||||
$mt = $settings.SelectSingleNode("d:MainTable", $ns)
|
||||
if ($mt) { $dynTable = " -> $($mt.InnerText)" }
|
||||
}
|
||||
|
||||
# ValueTable/ValueTree columns
|
||||
$colStr = ""
|
||||
$columns = $attr.SelectSingleNode("d:Columns", $ns)
|
||||
if ($columns -and ($typeStr -eq "ValueTable" -or $typeStr -eq "ValueTree")) {
|
||||
$cols = @()
|
||||
foreach ($col in $columns.SelectNodes("d:Column", $ns)) {
|
||||
$cName = $col.GetAttribute("name")
|
||||
$cTypeNode = $col.SelectSingleNode("d:Type", $ns)
|
||||
$cType = Format-Type $cTypeNode
|
||||
if ($cType) { $cols += "$cName`: $cType" } else { $cols += $cName }
|
||||
}
|
||||
if ($cols.Count -gt 0) {
|
||||
$colStr = " [$($cols -join ', ')]"
|
||||
}
|
||||
}
|
||||
|
||||
$line = " $prefix$aName`: $typeStr$colStr$dynTable$mainSuffix"
|
||||
if (-not $typeStr -and -not $colStr -and -not $dynTable) {
|
||||
$line = " $prefix$aName$mainSuffix"
|
||||
}
|
||||
$attrLines += $line
|
||||
}
|
||||
if ($attrLines.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "Attributes:"
|
||||
$lines += $attrLines
|
||||
}
|
||||
}
|
||||
|
||||
# --- Parameters ---
|
||||
|
||||
$paramsNode = $root.SelectSingleNode("d:Parameters", $ns)
|
||||
if ($paramsNode) {
|
||||
$paramLines = @()
|
||||
foreach ($param in $paramsNode.SelectNodes("d:Parameter", $ns)) {
|
||||
$pName = $param.GetAttribute("name")
|
||||
$typeNode = $param.SelectSingleNode("d:Type", $ns)
|
||||
$typeStr = Format-Type $typeNode
|
||||
|
||||
$keyParam = $param.SelectSingleNode("d:KeyParameter", $ns)
|
||||
$isKey = ($keyParam -and $keyParam.InnerText -eq "true")
|
||||
$keySuffix = if ($isKey) { " (key)" } else { "" }
|
||||
|
||||
if ($typeStr) {
|
||||
$paramLines += " $pName`: $typeStr$keySuffix"
|
||||
} else {
|
||||
$paramLines += " $pName$keySuffix"
|
||||
}
|
||||
}
|
||||
if ($paramLines.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "Parameters:"
|
||||
$lines += $paramLines
|
||||
}
|
||||
}
|
||||
|
||||
# --- Commands ---
|
||||
|
||||
$cmdsNode = $root.SelectSingleNode("d:Commands", $ns)
|
||||
if ($cmdsNode) {
|
||||
$cmdLines = @()
|
||||
foreach ($cmd in $cmdsNode.SelectNodes("d:Command", $ns)) {
|
||||
$cName = $cmd.GetAttribute("name")
|
||||
$shortcut = $cmd.SelectSingleNode("d:Shortcut", $ns)
|
||||
$scStr = if ($shortcut) { " [$($shortcut.InnerText)]" } else { "" }
|
||||
|
||||
# Collect all Action elements (may have multiple with callType)
|
||||
$actions = $cmd.SelectNodes("d:Action", $ns)
|
||||
if ($actions.Count -gt 1) {
|
||||
$actParts = @()
|
||||
foreach ($a in $actions) {
|
||||
$ct = $a.GetAttribute("callType")
|
||||
$ctStr = if ($ct) { "[$ct]" } else { "" }
|
||||
$actParts += "$($a.InnerText)$ctStr"
|
||||
}
|
||||
$actionStr = " -> $($actParts -join ', ')"
|
||||
} elseif ($actions.Count -eq 1) {
|
||||
$ct = $actions[0].GetAttribute("callType")
|
||||
$ctStr = if ($ct) { "[$ct]" } else { "" }
|
||||
$actionStr = " -> $($actions[0].InnerText)$ctStr"
|
||||
} else {
|
||||
$actionStr = ""
|
||||
}
|
||||
|
||||
$cmdLines += " $cName$actionStr$scStr"
|
||||
}
|
||||
if ($cmdLines.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "Commands:"
|
||||
$lines += $cmdLines
|
||||
}
|
||||
}
|
||||
|
||||
# --- BaseForm footer ---
|
||||
|
||||
if ($isExtension) {
|
||||
$bfVersion = $baseFormNode.GetAttribute("version")
|
||||
$bfStr = if ($bfVersion) { "present (version $bfVersion)" } else { "present" }
|
||||
$lines += ""
|
||||
$lines += "BaseForm: $bfStr"
|
||||
}
|
||||
|
||||
# --- Expand hint ---
|
||||
|
||||
if ($script:hasCollapsed) {
|
||||
$lines += ""
|
||||
$lines += "Hint: use -Expand <name> to expand a collapsed section, -Expand * for all"
|
||||
}
|
||||
|
||||
# --- Truncation protection ---
|
||||
|
||||
$totalLines = $lines.Count
|
||||
|
||||
if ($Offset -gt 0) {
|
||||
if ($Offset -ge $totalLines) {
|
||||
Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show."
|
||||
exit 0
|
||||
}
|
||||
$lines = $lines[$Offset..($totalLines - 1)]
|
||||
}
|
||||
|
||||
if ($lines.Count -gt $Limit) {
|
||||
$shown = $lines[0..($Limit - 1)]
|
||||
foreach ($l in $shown) { Write-Host $l }
|
||||
$remaining = $totalLines - $Offset - $Limit
|
||||
Write-Host ""
|
||||
Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue."
|
||||
} else {
|
||||
foreach ($l in $lines) { Write-Host $l }
|
||||
}
|
||||
@@ -0,0 +1,684 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-info v1.3 — Analyze 1C managed form structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from lxml import etree
|
||||
|
||||
# --- Namespace map ---
|
||||
|
||||
NSMAP = {
|
||||
"d": "http://v8.1c.ru/8.3/xcf/logform",
|
||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
||||
"v8ui": "http://v8.1c.ru/8.1/data/ui",
|
||||
"xr": "http://v8.1c.ru/8.3/xcf/readable",
|
||||
"xs": "http://www.w3.org/2001/XMLSchema",
|
||||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
"cfg": "http://v8.1c.ru/8.1/data/enterprise/current-config",
|
||||
"dcsset": "http://v8.1c.ru/8.1/data-composition-system/settings",
|
||||
}
|
||||
|
||||
# --- Skip elements ---
|
||||
|
||||
SKIP_ELEMENTS = {
|
||||
"ExtendedTooltip",
|
||||
"ContextMenu",
|
||||
"AutoCommandBar",
|
||||
"SearchStringAddition",
|
||||
"ViewStatusAddition",
|
||||
"SearchControlAddition",
|
||||
"ColumnGroup",
|
||||
}
|
||||
|
||||
|
||||
# --- Helper: extract multilang text ---
|
||||
|
||||
def get_ml_text(node):
|
||||
if node is None:
|
||||
return ""
|
||||
content = node.find("v8:item/v8:content", NSMAP)
|
||||
if content is not None and content.text:
|
||||
return content.text
|
||||
# Fallback: concatenate all text
|
||||
text = "".join(node.itertext()).strip()
|
||||
if text:
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
# --- Helper: format type compactly ---
|
||||
|
||||
def format_type(type_node):
|
||||
if type_node is None or len(type_node) == 0:
|
||||
return ""
|
||||
|
||||
type_set = type_node.find("v8:TypeSet", NSMAP)
|
||||
if type_set is not None:
|
||||
val = type_set.text or ""
|
||||
if val.startswith("cfg:"):
|
||||
val = val[4:]
|
||||
return val
|
||||
|
||||
types = type_node.findall("v8:Type", NSMAP)
|
||||
if len(types) == 0:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
for t in types:
|
||||
raw = t.text or ""
|
||||
if raw == "xs:string":
|
||||
sq = type_node.find("v8:StringQualifiers/v8:Length", NSMAP)
|
||||
length = int(sq.text) if sq is not None and sq.text else 0
|
||||
if length > 0:
|
||||
parts.append(f"string({length})")
|
||||
else:
|
||||
parts.append("string")
|
||||
elif raw == "xs:decimal":
|
||||
nq = type_node.find("v8:NumberQualifiers", NSMAP)
|
||||
if nq is not None:
|
||||
d = nq.find("v8:Digits", NSMAP)
|
||||
f = nq.find("v8:FractionDigits", NSMAP)
|
||||
digits = d.text if d is not None and d.text else "0"
|
||||
frac = f.text if f is not None and f.text else "0"
|
||||
parts.append(f"decimal({digits},{frac})")
|
||||
else:
|
||||
parts.append("decimal")
|
||||
elif raw == "xs:boolean":
|
||||
parts.append("boolean")
|
||||
elif raw == "xs:dateTime":
|
||||
dq = type_node.find("v8:DateQualifiers/v8:DateFractions", NSMAP)
|
||||
if dq is not None:
|
||||
frac_text = dq.text or ""
|
||||
if frac_text == "Date":
|
||||
parts.append("date")
|
||||
elif frac_text == "Time":
|
||||
parts.append("time")
|
||||
else:
|
||||
parts.append("dateTime")
|
||||
else:
|
||||
parts.append("dateTime")
|
||||
elif raw == "xs:binary":
|
||||
parts.append("binary")
|
||||
elif raw.startswith("cfg:") or re.match(r'^d\d+p\d+:', raw):
|
||||
parts.append(re.sub(r'^(?:cfg|d\d+p\d+):', '', raw))
|
||||
elif raw == "v8:ValueTable":
|
||||
parts.append("ValueTable")
|
||||
elif raw == "v8:ValueTree":
|
||||
parts.append("ValueTree")
|
||||
elif raw == "v8:ValueListType":
|
||||
parts.append("ValueList")
|
||||
elif raw == "v8:TypeDescription":
|
||||
parts.append("TypeDescription")
|
||||
elif raw == "v8:Universal":
|
||||
parts.append("Universal")
|
||||
elif raw == "v8:FixedArray":
|
||||
parts.append("FixedArray")
|
||||
elif raw == "v8:FixedStructure":
|
||||
parts.append("FixedStructure")
|
||||
elif raw == "v8ui:FormattedString":
|
||||
parts.append("FormattedString")
|
||||
elif raw == "v8ui:Picture":
|
||||
parts.append("Picture")
|
||||
elif raw == "v8ui:Color":
|
||||
parts.append("Color")
|
||||
elif raw == "v8ui:Font":
|
||||
parts.append("Font")
|
||||
elif raw.startswith("dcsset:"):
|
||||
parts.append(raw.replace("dcsset:", "DCS."))
|
||||
elif raw.startswith("dcssch:"):
|
||||
parts.append(raw.replace("dcssch:", "DCS."))
|
||||
elif raw.startswith("dcscor:"):
|
||||
parts.append(raw.replace("dcscor:", "DCS."))
|
||||
else:
|
||||
parts.append(raw)
|
||||
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
# --- Helper: check if title differs from name ---
|
||||
|
||||
def test_title_differs(node, name):
|
||||
title_node = node.find("d:Title", NSMAP)
|
||||
if title_node is None:
|
||||
return None
|
||||
title_text = get_ml_text(title_node)
|
||||
if not title_text:
|
||||
return None
|
||||
# Normalize: remove spaces, lowercase
|
||||
norm_title = title_text.replace(" ", "").lower()
|
||||
norm_name = name.lower()
|
||||
if norm_title == norm_name:
|
||||
return None
|
||||
return title_text
|
||||
|
||||
|
||||
# --- Helper: get events as compact string ---
|
||||
|
||||
def get_events_str(node):
|
||||
events_node = node.find("d:Events", NSMAP)
|
||||
if events_node is None:
|
||||
return ""
|
||||
evts = []
|
||||
for e in events_node.findall("d:Event", NSMAP):
|
||||
e_name = e.get("name", "")
|
||||
ct = e.get("callType", "")
|
||||
if ct:
|
||||
evts.append(f"{e_name}[{ct}]")
|
||||
else:
|
||||
evts.append(e_name)
|
||||
if len(evts) == 0:
|
||||
return ""
|
||||
return " {" + ", ".join(evts) + "}"
|
||||
|
||||
|
||||
# --- Helper: get flags ---
|
||||
|
||||
def get_flags(node):
|
||||
flags = []
|
||||
vis = node.find("d:Visible", NSMAP)
|
||||
if vis is not None and vis.text == "false":
|
||||
flags.append("visible:false")
|
||||
en = node.find("d:Enabled", NSMAP)
|
||||
if en is not None and en.text == "false":
|
||||
flags.append("enabled:false")
|
||||
ro = node.find("d:ReadOnly", NSMAP)
|
||||
if ro is not None and ro.text == "true":
|
||||
flags.append("ro")
|
||||
if len(flags) == 0:
|
||||
return ""
|
||||
return " [" + ",".join(flags) + "]"
|
||||
|
||||
|
||||
# --- Element type abbreviations ---
|
||||
|
||||
def get_element_tag(node):
|
||||
local_name = etree.QName(node.tag).localname
|
||||
if local_name == "UsualGroup":
|
||||
group_node = node.find("d:Group", NSMAP)
|
||||
orient = ""
|
||||
if group_node is not None:
|
||||
g_text = group_node.text or ""
|
||||
if g_text == "Vertical":
|
||||
orient = ":V"
|
||||
elif g_text == "Horizontal":
|
||||
orient = ":H"
|
||||
elif g_text == "AlwaysHorizontal":
|
||||
orient = ":AH"
|
||||
elif g_text == "AlwaysVertical":
|
||||
orient = ":AV"
|
||||
beh = node.find("d:Behavior", NSMAP)
|
||||
collapse = ""
|
||||
if beh is not None and beh.text == "Collapsible":
|
||||
collapse = ",collapse"
|
||||
return f"[Group{orient}{collapse}]"
|
||||
elif local_name == "InputField":
|
||||
return "[Input]"
|
||||
elif local_name == "CheckBoxField":
|
||||
return "[Check]"
|
||||
elif local_name == "LabelDecoration":
|
||||
return "[Label]"
|
||||
elif local_name == "LabelField":
|
||||
return "[LabelField]"
|
||||
elif local_name == "PictureDecoration":
|
||||
return "[Picture]"
|
||||
elif local_name == "PictureField":
|
||||
return "[PicField]"
|
||||
elif local_name == "CalendarField":
|
||||
return "[Calendar]"
|
||||
elif local_name == "Table":
|
||||
return "[Table]"
|
||||
elif local_name == "Button":
|
||||
return "[Button]"
|
||||
elif local_name == "CommandBar":
|
||||
return "[CmdBar]"
|
||||
elif local_name == "Pages":
|
||||
return "[Pages]"
|
||||
elif local_name == "Page":
|
||||
return "[Page]"
|
||||
elif local_name == "Popup":
|
||||
return "[Popup]"
|
||||
elif local_name == "ButtonGroup":
|
||||
return "[BtnGroup]"
|
||||
else:
|
||||
return f"[{local_name}]"
|
||||
|
||||
|
||||
# --- Count significant children (for Page summary) ---
|
||||
|
||||
def count_significant_children(child_items_node):
|
||||
if child_items_node is None:
|
||||
return 0
|
||||
count = 0
|
||||
for child in child_items_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
ln = etree.QName(child.tag).localname
|
||||
if ln in SKIP_ELEMENTS:
|
||||
continue
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
# --- Build element tree recursively ---
|
||||
|
||||
def build_tree(child_items_node, prefix, tree_lines, expand="", state=None):
|
||||
if child_items_node is None:
|
||||
return
|
||||
|
||||
# Collect significant children
|
||||
children = []
|
||||
for child in child_items_node:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
ln = etree.QName(child.tag).localname
|
||||
if ln in SKIP_ELEMENTS:
|
||||
continue
|
||||
children.append(child)
|
||||
|
||||
for i, child in enumerate(children):
|
||||
last = (i == len(children) - 1)
|
||||
connector = "\u2514\u2500" if last else "\u251C\u2500"
|
||||
continuation = " " if last else "\u2502 "
|
||||
|
||||
tag = get_element_tag(child)
|
||||
name = child.get("name", "")
|
||||
flags = get_flags(child)
|
||||
events = get_events_str(child)
|
||||
|
||||
# DataPath or CommandName
|
||||
binding = ""
|
||||
dp = child.find("d:DataPath", NSMAP)
|
||||
if dp is not None and dp.text:
|
||||
binding = f" -> {dp.text}"
|
||||
else:
|
||||
cn = child.find("d:CommandName", NSMAP)
|
||||
if cn is not None and cn.text:
|
||||
cn_val = cn.text
|
||||
m = re.match(r'^Form\.StandardCommand\.(.+)$', cn_val)
|
||||
if m:
|
||||
binding = f" -> {m.group(1)} [std]"
|
||||
else:
|
||||
m = re.match(r'^Form\.Command\.(.+)$', cn_val)
|
||||
if m:
|
||||
binding = f" -> {m.group(1)} [cmd]"
|
||||
else:
|
||||
binding = f" -> {cn_val}"
|
||||
|
||||
# Title differs?
|
||||
title_str = ""
|
||||
diff_title = test_title_differs(child, name)
|
||||
if diff_title:
|
||||
title_str = f" [title:{diff_title}]"
|
||||
|
||||
line = f"{prefix}{connector} {tag} {name}{binding}{flags}{title_str}{events}"
|
||||
tree_lines.append(line)
|
||||
|
||||
# Recurse into containers (but not Page -- show summary unless expanded)
|
||||
local_name = etree.QName(child.tag).localname
|
||||
if local_name == "Page":
|
||||
ci = child.find("d:ChildItems", NSMAP)
|
||||
page_name = child.get("name", "")
|
||||
page_title = test_title_differs(child, page_name)
|
||||
should_expand = (expand == "*") or (expand == page_name) or (page_title and expand == page_title)
|
||||
if should_expand and ci is not None:
|
||||
build_tree(ci, prefix + continuation, tree_lines, expand, state)
|
||||
else:
|
||||
cnt = count_significant_children(ci)
|
||||
tree_lines[-1] = tree_lines[-1] + f" ({cnt} items)"
|
||||
if state is not None:
|
||||
state["has_collapsed"] = True
|
||||
elif local_name in ("UsualGroup", "Pages", "Table", "CommandBar", "ButtonGroup", "Popup"):
|
||||
ci = child.find("d:ChildItems", NSMAP)
|
||||
if ci is not None:
|
||||
build_tree(ci, prefix + continuation, tree_lines, expand, state)
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Analyze 1C managed form structure", allow_abbrev=False)
|
||||
parser.add_argument("-FormPath", "-Path", required=True, help="Path to Form.xml")
|
||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
|
||||
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
|
||||
args = parser.parse_args()
|
||||
|
||||
form_path = args.FormPath
|
||||
limit = args.Limit
|
||||
offset = args.Offset
|
||||
expand = args.Expand
|
||||
|
||||
# --- Resolve FormPath ---
|
||||
if not os.path.isabs(form_path):
|
||||
form_path = os.path.join(os.getcwd(), form_path)
|
||||
# A: Directory → Ext/Form.xml
|
||||
if os.path.isdir(form_path):
|
||||
form_path = os.path.join(form_path, "Ext", "Form.xml")
|
||||
# B1: Missing Ext/ (Forms/Форма/Form.xml → Forms/Форма/Ext/Form.xml)
|
||||
if not os.path.isfile(form_path):
|
||||
fn = os.path.basename(form_path)
|
||||
if fn == "Form.xml":
|
||||
c = os.path.join(os.path.dirname(form_path), "Ext", fn)
|
||||
if os.path.isfile(c):
|
||||
form_path = c
|
||||
# B2: Descriptor (Forms/Форма.xml → Forms/Форма/Ext/Form.xml)
|
||||
if not os.path.isfile(form_path) and form_path.endswith(".xml"):
|
||||
stem = os.path.splitext(os.path.basename(form_path))[0]
|
||||
parent = os.path.dirname(form_path)
|
||||
c = os.path.join(parent, stem, "Ext", "Form.xml")
|
||||
if os.path.isfile(c):
|
||||
form_path = c
|
||||
|
||||
if not os.path.isfile(form_path):
|
||||
print(f"File not found: {form_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Load XML ---
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(form_path, parser_xml)
|
||||
root = tree.getroot()
|
||||
|
||||
# --- Detect extension (BaseForm) ---
|
||||
base_form_node = root.find("d:BaseForm", NSMAP)
|
||||
is_extension = base_form_node is not None
|
||||
|
||||
# --- Determine form name and object from path ---
|
||||
resolved_path = os.path.abspath(form_path)
|
||||
parts = resolved_path.replace("\\", "/").split("/")
|
||||
|
||||
form_name = ""
|
||||
object_context = ""
|
||||
|
||||
# Look for /Forms/<FormName>/Ext/Form.xml pattern
|
||||
forms_idx = -1
|
||||
for i in range(len(parts) - 1, -1, -1):
|
||||
if parts[i] == "Forms":
|
||||
forms_idx = i
|
||||
break
|
||||
|
||||
if forms_idx >= 0 and (forms_idx + 1) < len(parts):
|
||||
form_name = parts[forms_idx + 1]
|
||||
# Object is 2 levels up: .../<ObjectType>/<ObjectName>/Forms/...
|
||||
if forms_idx >= 2:
|
||||
obj_type = parts[forms_idx - 2]
|
||||
obj_name = parts[forms_idx - 1]
|
||||
object_context = f"{obj_type}.{obj_name}"
|
||||
else:
|
||||
# CommonForms pattern: .../<ObjectType>/<FormName>/Ext/Form.xml
|
||||
ext_idx = -1
|
||||
for i in range(len(parts) - 1, -1, -1):
|
||||
if parts[i] == "Ext":
|
||||
ext_idx = i
|
||||
break
|
||||
if ext_idx >= 2:
|
||||
form_name = parts[ext_idx - 1]
|
||||
obj_type = parts[ext_idx - 2]
|
||||
object_context = obj_type
|
||||
else:
|
||||
form_name = os.path.splitext(os.path.basename(form_path))[0]
|
||||
|
||||
# --- Collect output ---
|
||||
lines = []
|
||||
|
||||
# Header -- include Title if present
|
||||
title_node = root.find("d:Title", NSMAP)
|
||||
form_title = None
|
||||
if title_node is not None:
|
||||
form_title = get_ml_text(title_node)
|
||||
if not form_title:
|
||||
form_title = "".join(title_node.itertext()).strip() or None
|
||||
|
||||
ext_marker = " [EXTENSION]" if is_extension else ""
|
||||
header = f"=== Form: {form_name}{ext_marker}"
|
||||
if form_title:
|
||||
header += f' — "{form_title}"'
|
||||
if object_context:
|
||||
header += f" ({object_context})"
|
||||
header += " ==="
|
||||
lines.append(header)
|
||||
|
||||
# --- Form properties (Title excluded -- shown in header) ---
|
||||
prop_names = [
|
||||
"Width", "Height", "Group",
|
||||
"WindowOpeningMode", "EnterKeyBehavior", "AutoTitle", "AutoURL",
|
||||
"AutoFillCheck", "Customizable", "CommandBarLocation",
|
||||
"SaveDataInSettings", "AutoSaveDataInSettings",
|
||||
"AutoTime", "UsePostingMode", "RepostOnWrite",
|
||||
"UseForFoldersAndItems",
|
||||
"ReportResult", "DetailsData", "ReportFormType",
|
||||
"VerticalScroll", "ScalingMode",
|
||||
]
|
||||
|
||||
props = []
|
||||
for pn in prop_names:
|
||||
p_node = root.find(f"d:{pn}", NSMAP)
|
||||
if p_node is not None:
|
||||
val = get_ml_text(p_node)
|
||||
if not val:
|
||||
val = "".join(p_node.itertext()).strip()
|
||||
props.append(f"{pn}={val}")
|
||||
|
||||
if len(props) > 0:
|
||||
lines.append("")
|
||||
lines.append("Properties: " + ", ".join(props))
|
||||
|
||||
# --- Excluded commands ---
|
||||
excluded_cmds = []
|
||||
for ec in root.findall("d:CommandSet/d:ExcludedCommand", NSMAP):
|
||||
excluded_cmds.append(ec.text or "")
|
||||
|
||||
# --- Form events ---
|
||||
form_events = root.find("d:Events", NSMAP)
|
||||
if form_events is not None and len(form_events) > 0:
|
||||
lines.append("")
|
||||
lines.append("Events:")
|
||||
for e in form_events.findall("d:Event", NSMAP):
|
||||
e_name = e.get("name", "")
|
||||
e_handler = e.text or ""
|
||||
ct = e.get("callType", "")
|
||||
ct_str = f"[{ct}]" if ct else ""
|
||||
lines.append(f" {e_name}{ct_str} -> {e_handler}")
|
||||
|
||||
# --- Main AutoCommandBar (form's id=-1 panel) ---
|
||||
def format_main_acb(acb_node):
|
||||
if acb_node is None:
|
||||
return []
|
||||
autofill_node = acb_node.find("d:Autofill", NSMAP)
|
||||
autofill = not (autofill_node is not None and autofill_node.text == "false")
|
||||
halign_node = acb_node.find("d:HorizontalAlign", NSMAP)
|
||||
flags = ["autofill" if autofill else "no-autofill"]
|
||||
if halign_node is not None and halign_node.text:
|
||||
flags.append(f"align={halign_node.text}")
|
||||
ci_node = acb_node.find("d:ChildItems", NSMAP)
|
||||
buttons = []
|
||||
if ci_node is not None:
|
||||
for btn in ci_node:
|
||||
if not isinstance(btn.tag, str):
|
||||
continue
|
||||
ln = etree.QName(btn).localname
|
||||
if ln in SKIP_ELEMENTS:
|
||||
continue
|
||||
b_name = btn.get("name", "")
|
||||
cmd_node = btn.find("d:CommandName", NSMAP)
|
||||
cmd_ref = cmd_node.text if cmd_node is not None and cmd_node.text else ""
|
||||
loc_node = btn.find("d:LocationInCommandBar", NSMAP)
|
||||
loc_str = f" [{loc_node.text}]" if loc_node is not None and loc_node.text else ""
|
||||
tag = get_element_tag(btn)
|
||||
if cmd_ref:
|
||||
buttons.append(f" {tag} {b_name} -> {cmd_ref}{loc_str}")
|
||||
else:
|
||||
buttons.append(f" {tag} {b_name}{loc_str}")
|
||||
if not buttons and autofill and halign_node is None:
|
||||
return ["AutoCommandBar [autofill]"]
|
||||
return [f"AutoCommandBar [{', '.join(flags)}]"] + buttons
|
||||
|
||||
cb_loc_node = root.find("d:CommandBarLocation", NSMAP)
|
||||
cb_loc = cb_loc_node.text if cb_loc_node is not None and cb_loc_node.text else "Auto"
|
||||
main_acb_node = root.find("d:AutoCommandBar", NSMAP)
|
||||
acb_lines = []
|
||||
if cb_loc != "None" and main_acb_node is not None:
|
||||
acb_lines = format_main_acb(main_acb_node)
|
||||
|
||||
if acb_lines and cb_loc in ("Auto", "Top"):
|
||||
lines.append("")
|
||||
lines.extend(acb_lines)
|
||||
|
||||
# --- Element tree ---
|
||||
tree_state = {"has_collapsed": False}
|
||||
child_items = root.find("d:ChildItems", NSMAP)
|
||||
if child_items is not None:
|
||||
lines.append("")
|
||||
lines.append("Elements:")
|
||||
tree_lines = []
|
||||
build_tree(child_items, " ", tree_lines, expand, tree_state)
|
||||
lines.extend(tree_lines)
|
||||
|
||||
if acb_lines and cb_loc == "Bottom":
|
||||
lines.append("")
|
||||
lines.extend(acb_lines)
|
||||
|
||||
# --- Attributes ---
|
||||
attrs_node = root.find("d:Attributes", NSMAP)
|
||||
if attrs_node is not None:
|
||||
attr_lines = []
|
||||
for attr in attrs_node.findall("d:Attribute", NSMAP):
|
||||
a_name = attr.get("name", "")
|
||||
type_node = attr.find("d:Type", NSMAP)
|
||||
type_str = format_type(type_node)
|
||||
|
||||
main_attr = attr.find("d:MainAttribute", NSMAP)
|
||||
is_main = main_attr is not None and main_attr.text == "true"
|
||||
|
||||
prefix_char = "*" if is_main else " "
|
||||
main_suffix = " (main)" if is_main else ""
|
||||
|
||||
# DynamicList: show MainTable
|
||||
settings = attr.find("d:Settings", NSMAP)
|
||||
dyn_table = ""
|
||||
if settings is not None and type_str == "DynamicList":
|
||||
mt = settings.find("d:MainTable", NSMAP)
|
||||
if mt is not None and mt.text:
|
||||
dyn_table = f" -> {mt.text}"
|
||||
|
||||
# ValueTable/ValueTree columns
|
||||
col_str = ""
|
||||
columns = attr.find("d:Columns", NSMAP)
|
||||
if columns is not None and type_str in ("ValueTable", "ValueTree"):
|
||||
cols = []
|
||||
for col in columns.findall("d:Column", NSMAP):
|
||||
c_name = col.get("name", "")
|
||||
c_type_node = col.find("d:Type", NSMAP)
|
||||
c_type = format_type(c_type_node)
|
||||
if c_type:
|
||||
cols.append(f"{c_name}: {c_type}")
|
||||
else:
|
||||
cols.append(c_name)
|
||||
if len(cols) > 0:
|
||||
col_str = " [" + ", ".join(cols) + "]"
|
||||
|
||||
if type_str or col_str or dyn_table:
|
||||
line = f" {prefix_char}{a_name}: {type_str}{col_str}{dyn_table}{main_suffix}"
|
||||
else:
|
||||
line = f" {prefix_char}{a_name}{main_suffix}"
|
||||
attr_lines.append(line)
|
||||
|
||||
if len(attr_lines) > 0:
|
||||
lines.append("")
|
||||
lines.append("Attributes:")
|
||||
lines.extend(attr_lines)
|
||||
|
||||
# --- Parameters ---
|
||||
params_node = root.find("d:Parameters", NSMAP)
|
||||
if params_node is not None:
|
||||
param_lines = []
|
||||
for param in params_node.findall("d:Parameter", NSMAP):
|
||||
p_name = param.get("name", "")
|
||||
type_node = param.find("d:Type", NSMAP)
|
||||
type_str = format_type(type_node)
|
||||
|
||||
key_param = param.find("d:KeyParameter", NSMAP)
|
||||
is_key = key_param is not None and key_param.text == "true"
|
||||
key_suffix = " (key)" if is_key else ""
|
||||
|
||||
if type_str:
|
||||
param_lines.append(f" {p_name}: {type_str}{key_suffix}")
|
||||
else:
|
||||
param_lines.append(f" {p_name}{key_suffix}")
|
||||
|
||||
if len(param_lines) > 0:
|
||||
lines.append("")
|
||||
lines.append("Parameters:")
|
||||
lines.extend(param_lines)
|
||||
|
||||
# --- Commands ---
|
||||
cmds_node = root.find("d:Commands", NSMAP)
|
||||
if cmds_node is not None:
|
||||
cmd_lines = []
|
||||
for cmd in cmds_node.findall("d:Command", NSMAP):
|
||||
c_name = cmd.get("name", "")
|
||||
shortcut = cmd.find("d:Shortcut", NSMAP)
|
||||
sc_str = f" [{shortcut.text}]" if shortcut is not None and shortcut.text else ""
|
||||
|
||||
# Collect all Action elements (may have multiple with callType)
|
||||
actions = cmd.findall("d:Action", NSMAP)
|
||||
if len(actions) > 1:
|
||||
act_parts = []
|
||||
for a in actions:
|
||||
ct = a.get("callType", "")
|
||||
ct_str = f"[{ct}]" if ct else ""
|
||||
act_parts.append(f"{a.text or ''}{ct_str}")
|
||||
action_str = " -> " + ", ".join(act_parts)
|
||||
elif len(actions) == 1:
|
||||
ct = actions[0].get("callType", "")
|
||||
ct_str = f"[{ct}]" if ct else ""
|
||||
action_str = f" -> {actions[0].text or ''}{ct_str}"
|
||||
else:
|
||||
action_str = ""
|
||||
|
||||
cmd_lines.append(f" {c_name}{action_str}{sc_str}")
|
||||
|
||||
if len(cmd_lines) > 0:
|
||||
lines.append("")
|
||||
lines.append("Commands:")
|
||||
lines.extend(cmd_lines)
|
||||
|
||||
# --- BaseForm footer ---
|
||||
if is_extension:
|
||||
bf_version = base_form_node.get("version", "")
|
||||
bf_str = f"present (version {bf_version})" if bf_version else "present"
|
||||
lines.append("")
|
||||
lines.append(f"BaseForm: {bf_str}")
|
||||
|
||||
# --- Expand hint ---
|
||||
if tree_state["has_collapsed"]:
|
||||
lines.append("")
|
||||
lines.append("Hint: use -Expand <name> to expand a collapsed section, -Expand * for all")
|
||||
|
||||
# --- Truncation protection ---
|
||||
total_lines = len(lines)
|
||||
|
||||
if offset > 0:
|
||||
if offset >= total_lines:
|
||||
print(f"[INFO] Offset {offset} exceeds total lines ({total_lines}). Nothing to show.")
|
||||
sys.exit(0)
|
||||
lines = lines[offset:]
|
||||
|
||||
if len(lines) > limit:
|
||||
shown = lines[:limit]
|
||||
for l in shown:
|
||||
print(l)
|
||||
remaining = total_lines - offset - limit
|
||||
print("")
|
||||
print(f"[TRUNCATED] Shown {limit} of {total_lines} lines. Use -Offset {offset + limit} to continue.")
|
||||
else:
|
||||
for l in lines:
|
||||
print(l)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user